Lightweight 0.20260625.0
Loading...
Searching...
No Matches
DataMapper.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../Async/Backend.hpp"
5#include "../SqlConnection.hpp"
6#include "../SqlDataBinder.hpp"
7#include "../SqlLogger.hpp"
8#include "../SqlRealName.hpp"
9#include "../SqlStatement.hpp"
10#include "../Utils.hpp"
11#include "BelongsTo.hpp"
12#include "CollectDifferences.hpp"
13#include "Field.hpp"
14#include "HasMany.hpp"
15#include "HasManyThrough.hpp"
16#include "HasOneThrough.hpp"
17#include "QueryBuilders.hpp"
18#include "Record.hpp"
19
20#include <reflection-cpp/reflection.hpp>
21
22#include <cassert>
23#include <concepts>
24#include <memory>
25#include <ranges>
26#include <tuple>
27#include <type_traits>
28#include <utility>
29#include <vector>
30
31namespace Lightweight
32{
33
34/// @defgroup DataMapper Data Mapper
35///
36/// @brief The data mapper is a high level API for mapping records to and from the database using high level C++ syntax.
37
38namespace detail
39{
40 // Converts a container of T to a container of std::shared_ptr<T>.
41 template <template <typename> class Allocator, template <typename, typename> class Container, typename Object>
42 auto ToSharedPtrList(Container<Object, Allocator<Object>> container)
43 {
44 using SharedPtrRecord = std::shared_ptr<Object>;
45 auto sharedPtrContainer = Container<SharedPtrRecord, Allocator<SharedPtrRecord>> {};
46 for (auto& object: container)
47 sharedPtrContainer.emplace_back(std::make_shared<Object>(std::move(object)));
48 return sharedPtrContainer;
49 }
50} // namespace detail
51
52/// @brief Main API for mapping records to and from the database using high level C++ syntax.
53///
54/// A DataMapper instances operates on a single SQL connection and provides methods to
55/// create, read, update and delete records in the database.
56///
57/// @see Field, BelongsTo, HasMany, HasManyThrough, HasOneThrough
58/// @ingroup DataMapper
59///
60/// @code
61/// struct Person
62/// {
63/// Field<SqlGuid, PrimaryKey::AutoAssign> id;
64/// Field<SqlAnsiString<30>> name;
65/// Field<SqlAnsiString<40>> email;
66/// };
67///
68/// auto dm = DataMapper {};
69///
70/// // Create a new person record
71/// auto person = Person { .id = SqlGuid::Create(), .name = "John Doe", .email = "johnt@doe.com" };
72///
73/// // Create the record in the database and set the primary key on the record
74/// auto const personId = dm.Create(person);
75///
76/// // Query the person record from the database
77/// auto const queriedPerson = dm.Query<Person>(personId)
78/// .Where(FieldNameOf<&Person::id>, "=", personId)
79/// .First();
80///
81/// if (queriedPerson.has_value())
82/// std::println("Queried Person: {}", DataMapper::Inspect(queriedPerson.value()));
83///
84/// // Update the person record in the database
85/// person.email = "alt@doe.com";
86/// dm.Update(person);
87///
88/// // Delete the person record from the database
89/// dm.Delete(person);
90/// @endcode
92{
93 public:
94 /// Acquires a thread-local DataMapper instance that is safe for reuse within that thread.
95 LIGHTWEIGHT_API static DataMapper& AcquireThreadLocal();
96
97 /// Constructs a new data mapper, using the default connection.
99 _connection {},
100 _stmt { _connection }
101 {
102 }
103
104 /// Constructs a new data mapper, using the given connection.
105 explicit DataMapper(SqlConnection&& connection):
106 _connection { std::move(connection) },
107 _stmt { _connection }
108 {
109 }
110
111 /// Constructs a new data mapper, using the given connection string.
112 explicit DataMapper(std::optional<SqlConnectionString> connectionString):
113 _connection { std::move(connectionString) },
114 _stmt { _connection }
115 {
116 }
117
118 DataMapper(DataMapper const&) = delete;
119 DataMapper& operator=(DataMapper const&) = delete;
120
121 /// Move constructor.
122 DataMapper(DataMapper&& other) noexcept:
123 _connection(std::move(other._connection)),
124 _stmt(_connection)
125 {
126 other._stmt = SqlStatement(std::nullopt);
127 }
128
129 /// Move assignment operator.
130 DataMapper& operator=(DataMapper&& other) noexcept
131 {
132 if (this == &other)
133 return *this;
134
135 _connection = std::move(other._connection);
136 _stmt = SqlStatement(_connection);
137 other._stmt = SqlStatement(std::nullopt);
138
139 return *this;
140 }
141
142 ~DataMapper() = default;
143
144 /// Returns the connection reference used by this data mapper.
145 [[nodiscard]] SqlConnection const& Connection() const noexcept
146 {
147 return _connection;
148 }
149
150 /// Returns the mutable connection reference used by this data mapper.
151 [[nodiscard]] SqlConnection& Connection() noexcept
152 {
153 return _connection;
154 }
155
156#if defined(BUILD_TESTS)
157
158 [[nodiscard]] SqlStatement& Statement(this auto&& self) noexcept
159 {
160 return self._stmt;
161 }
162
163#endif
164
165 /// Constructs a human readable string representation of the given record.
166 template <typename Record>
167 static std::string Inspect(Record const& record);
168
169 /// Constructs a string list of SQL queries to create the table for the given record type.
170 template <typename Record>
171 std::vector<std::string> CreateTableString(SqlServerType serverType);
172
173 /// Constructs a string list of SQL queries to create the tables for the given record types.
174 template <typename FirstRecord, typename... MoreRecords>
175 std::vector<std::string> CreateTablesString(SqlServerType serverType);
176
177 /// Creates the table for the given record type.
178 template <typename Record>
179 void CreateTable();
180
181 /// Creates the tables for the given record types.
182 template <typename FirstRecord, typename... MoreRecords>
183 void CreateTables();
184
185 /// @brief Creates a new record in the database.
186 ///
187 /// The record is inserted into the database and the primary key is set on this record.
188 ///
189 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
190 /// @tparam Record The record type to insert.
191 /// @param record The record to insert. The primary key field is updated in-place after the insert.
192 /// @return The primary key of the newly created record.
193 template <DataMapperOptions QueryOptions = {}, typename Record>
194 RecordPrimaryKeyType<Record> Create(Record& record);
195
196 /// @brief Creates a new record in the database.
197 ///
198 /// @note This is a variation of the Create() method and does not update the record's primary key.
199 ///
200 /// @tparam Record The record type to insert.
201 /// @param record The record to insert. Unlike Create(), the primary key field is NOT updated in-place.
202 /// @return The primary key of the newly created record.
203 template <typename Record>
204 RecordPrimaryKeyType<Record> CreateExplicit(Record const& record);
205
206 /// @brief Batch-inserts a span of records with a single prepared statement.
207 ///
208 /// The INSERT is prepared once and the whole batch is submitted via
209 /// SqlStatement::ExecuteBatch(rows, accessors...), which uses native zero-copy row-wise array
210 /// binding when every inserted column is row-bindable (primitives, date/time/datetime, numeric, or
211 /// std::optional of a fixed non-numeric type) and the driver supports parameter arrays, otherwise a
212 /// prepare-once + per-row execute. This is dramatically faster than calling CreateExplicit() in a
213 /// loop (which re-prepares per row).
214 ///
215 /// @note Like CreateExplicit(), this does not write back primary keys, relations, or modified-state
216 /// onto the records; callers should treat the inserted records as write-only inputs. Auto-increment
217 /// primary keys are not retrieved.
218 ///
219 /// Accepts any contiguous, sized range of records (e.g. std::vector, std::array, std::span, or a C
220 /// array), so `dm.CreateAll(records)` works without an explicit std::span wrapper. Non-contiguous
221 /// ranges are rejected at compile time via static_assert (no implicit copy is made).
222 ///
223 /// @tparam Records A contiguous range whose element type is the record type to insert.
224 /// @param records The records to insert. An empty range is a no-op.
225 template <std::ranges::range Records>
226 void CreateAll(Records const& records);
227
228 /// @brief Creates a copy of an existing record in the database.
229 ///
230 /// This method is useful for duplicating a database record while assigning a new primary key.
231 /// All fields except primary key(s) are copied from the original record.
232 /// The primary key is automatically generated (auto-incremented or auto-assigned).
233 ///
234 /// @param originalRecord The record to copy.
235 /// @return The primary key of the newly created record.
236 template <DataMapperOptions QueryOptions = {}, typename Record>
237 [[nodiscard]] RecordPrimaryKeyType<Record> CreateCopyOf(Record const& originalRecord);
238
239 /// @brief Queries a single record (based on primary key) from the database.
240 ///
241 /// The primary key(s) are used to identify the record to load.
242 /// If the record is not found, std::nullopt is returned.
243 ///
244 /// @tparam Record The record type to query and materialize.
245 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior,
246 /// such as whether related records should be auto-loaded. For example,
247 /// set the relation loading option to false to disable auto-loading of
248 /// relations when reading a single record.
249 /// @tparam PrimaryKeyTypes The type(s) of the primary key value(s) used to look up the record.
250 /// @param primaryKeys The primary key value(s) identifying the record to load.
251 /// @return An initialized Record if found; otherwise std::nullopt.
252 ///
253 /// @code
254 /// // Example: disable auto-loading of relations when querying a single record
255 /// auto result = dataMapper
256 /// .QuerySingle<MyRecord, DataMapperOptions{ .loadRelations = false }>(primaryKeyValue);
257 /// if (result)
258 /// {
259 /// // use *result; relations have not been auto-loaded
260 /// }
261 /// @endcode
262 template <typename Record, DataMapperOptions QueryOptions = {}, typename... PrimaryKeyTypes>
263 std::optional<Record> QuerySingle(PrimaryKeyTypes&&... primaryKeys);
264
265 /// Queries multiple records from the database, based on the given query.
266 ///
267 /// @tparam Record The record type to query and materialize.
268 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
269 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
270 /// @param selectQuery The composed SQL select query to execute.
271 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
272 /// @return A vector of records populated from the query results.
273 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
274 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
275
276 /// Queries multiple records from the database, based on the given query.
277 ///
278 /// @param sqlQueryString The SQL query string to execute.
279 /// @param inputParameters The input parameters for the query to be bound before executing.
280 /// @return A vector of records of the given type that were found via the query.
281 ///
282 /// example:
283 /// @code
284 /// struct Person
285 /// {
286 /// int id;
287 /// std::string name;
288 /// std::string email;
289 /// std::string phone;
290 /// std::string address;
291 /// std::string city;
292 /// std::string country;
293 /// };
294 ///
295 /// void example(DataMapper& dm)
296 /// {
297 /// auto const sqlQueryString = R"(SELECT * FROM "Person" WHERE "city" = ? AND "country" = ?)";
298 /// auto const records = dm.Query<Person>(sqlQueryString, "Berlin", "Germany");
299 /// for (auto const& record: records)
300 /// {
301 /// std::println("Person: {}", DataMapper::Inspect(record));
302 /// }
303 /// }
304 /// @endcode
305 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
306 std::vector<Record> Query(std::string_view sqlQueryString, InputParameters&&... inputParameters);
307
308 /// Queries records from the database, based on the given query and can be used to retrieve only part of the record
309 /// by specifying the ElementMask.
310 ///
311 /// @tparam ElementMask A SqlElements<Idx...> specialization specifying the zero-based field indices to populate.
312 /// @tparam Record The record type to query and materialize.
313 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
314 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
315 /// @param selectQuery The composed SQL select query to execute. Only the columns listed in the SELECT clause
316 /// are bound; the remaining fields of Record are left at their default values.
317 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
318 /// @return A vector of partially populated records; only fields at the specified indices are filled in.
319 ///
320 /// @code
321 ///
322 /// struct Person
323 /// {
324 /// Field<int> id;
325 /// Field<std::string> name; // index 1
326 /// Field<std::string> email;
327 /// Field<std::string> phone;
328 /// Field<std::string> address;
329 /// Field<std::string> city; // index 5
330 /// Field<std::string> country;
331 /// };
332 ///
333 /// void example(DataMapper& dm)
334 /// {
335 /// auto const query = dm.FromTable(RecordTableName<Person>)
336 /// .Select()
337 /// .Fields({ "name"sv, "city"sv })
338 /// .All();
339 /// auto const infos = dm.Query<SqlElements<1, 5>, Person>(query);
340 /// for (auto const& info : infos)
341 /// {
342 /// // only info.name and info.city are populated
343 /// }
344 /// }
345 /// @endcode
346 template <typename ElementMask, typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
347 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
348
349 /// Queries records of different types from the database, based on the given query.
350 /// User can constructed query that selects columns from the multiple tables
351 /// this function is used to get result of the query
352 ///
353 /// @tparam First The first record type to materialize from each result row.
354 /// @tparam Second The second record type to materialize from each result row.
355 /// @tparam Rest Zero or more additional record types to materialize from each result row.
356 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
357 /// @param selectQuery The composed SQL select query whose column list covers all fields of First, Second, and Rest.
358 /// @return A vector of tuples, each containing one instance of every requested record type per result row.
359 ///
360 /// @code
361 ///
362 /// struct JointA{};
363 /// struct JointB{};
364 /// struct JointC{};
365 ///
366 /// // the following query will construct statement to fetch all elements of JointA and JointC types
367 /// auto dm = DataMapper {};
368 /// auto const query = dm.FromTable(RecordTableName<JoinTestA>)
369 /// .Select()
370 /// .Fields<JointA, JointC>()
371 /// .InnerJoin<&JointB::a_id, &JointA::id>()
372 /// .InnerJoin<&JointC::id, &JointB::c_id>()
373 /// .All();
374 /// auto const records = dm.Query<JointA, JointC>(query);
375 /// for(const auto [elementA, elementC] : records)
376 /// {
377 /// // do something with elementA and elementC
378 /// }
379 /// @endcode
380 template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions = {}>
381 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
382 std::vector<std::tuple<First, Second, Rest...>> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery);
383
384 /// Queries records of given Record type.
385 ///
386 /// The query builder can be used to further refine the query.
387 /// The query builder will execute the query when a method like All(), First(n), etc. is called.
388 ///
389 /// @tparam Record The record type to query and materialize.
390 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
391 /// @return A query builder for the given Record type.
392 ///
393 /// @code
394 /// auto const records = dm.Query<Person>()
395 /// .Where(FieldNameOf<&Person::is_active>, "=", true)
396 /// .All();
397 /// @endcode
398 template <typename Record, DataMapperOptions QueryOptions = {}>
400 {
401 return SqlAllFieldsQueryBuilder<Record, QueryOptions>(*this, BuildFullyQualifiedFieldList<Record>());
402 }
403
404 /// Asynchronous counterpart of @c Query — returns an async query builder for @p Record.
405 ///
406 /// The builder offers the exact same fluent DSL (`Where`, `OrderBy`, `GroupBy`, joins, …) as the
407 /// synchronous one; its finisher methods (`All()`, `First()`, `First(n)`, `Range()`, `Count()`,
408 /// `Exist()`, `Delete()`) return an @c Async::Task instead of the plain result, to be @c co_await -ed.
409 /// The connection must have been put into async mode via @c SqlConnection::EnableAsync first.
410 ///
411 /// @note The returned builder is a temporary; keep the whole chain in the @c co_await full-expression
412 /// (e.g. `co_await dm.QueryAsync<Person>().Where(...).All();`) so it outlives the awaited task.
413 ///
414 /// @tparam Record The record type to query and materialize.
415 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
416 /// @return An asynchronous query builder for the given Record type.
417 ///
418 /// @code
419 /// auto const records = co_await dm.QueryAsync<Person>()
420 /// .Where(FieldNameOf<&Person::is_active>, "=", true)
421 /// .All();
422 /// @endcode
423 template <typename Record, DataMapperOptions QueryOptions = {}>
429
430 /// Returns a SqlQueryBuilder using the default query formatter.
431 ///
432 /// This can be used to build custom queries separately from the DataMapper
433 /// and execute them via the DataMapper's typed Query() overloads that accept a SqlSelectQueryBuilder.
434 ///
435 /// @return A SqlQueryBuilder bound to the connection's query formatter.
437 {
438 return SqlQueryBuilder(_connection.QueryFormatter());
439 }
440
441 /// Updates the record in the database.
442 ///
443 /// Only fields that have been modified since the record was last loaded or saved are written.
444 /// Fields that were not changed are excluded from the UPDATE statement.
445 ///
446 /// @tparam Record The record type to update.
447 /// @param record The record to update. Only its modified fields are written to the database.
448 template <typename Record>
449 void Update(Record& record);
450
451 /// @brief Batch-updates a span of records with a single prepared statement.
452 ///
453 /// One UPDATE is prepared that writes **all** storable non-primary-key columns of the record,
454 /// matched on the primary key(s) (`UPDATE … SET <all non-PK columns> WHERE <pk> = ?`), and the whole
455 /// batch is submitted via SqlStatement::ExecuteBatch(rows, accessors...) — natively row-wise when
456 /// possible, otherwise prepare-once + per-row execute.
457 ///
458 /// @note Unlike Update(), which writes only the modified fields of a single record, this writes a
459 /// uniform set of columns for every row, because a single prepared statement must bind the same
460 /// columns for the whole batch. Per-row modified-state is therefore not consulted, and is not reset.
461 ///
462 /// Accepts any contiguous, sized range of records (see CreateAll), so `dm.UpdateAll(records)` works
463 /// without an explicit std::span wrapper. Non-contiguous ranges are rejected at compile time.
464 ///
465 /// @tparam Records A contiguous range whose element type is the record type to update (with a primary key).
466 /// @param records The records to update. An empty range is a no-op.
467 template <std::ranges::range Records>
468 void UpdateAll(Records const& records);
469
470 /// Deletes the record from the database.
471 ///
472 /// The record is identified by its primary key(s). The row is removed from the backing table.
473 ///
474 /// @tparam Record The record type to delete.
475 /// @param record The record to delete. Its primary key field(s) identify the row to remove.
476 /// @return The number of rows deleted (typically 1 if the record was found, 0 otherwise).
477 template <typename Record>
478 std::size_t Delete(Record const& record);
479
480 /// Constructs an SQL query builder for the given table name.
481 SqlQueryBuilder FromTable(std::string_view tableName)
482 {
483 return _connection.Query(tableName);
484 }
485
486 /// Checks if the record has any modified fields.
487 ///
488 /// @tparam Record The record type to inspect.
489 /// @param record The record to check.
490 /// @return True if at least one field has been modified since the record was last loaded or saved.
491 template <typename Record>
492 bool IsModified(Record const& record) const noexcept;
493
494 /// Enum to set the modified state of a record.
495 enum class ModifiedState : uint8_t
496 {
497 Modified,
498 NotModified
499 };
500
501 /// Sets the modified state of the record after receiving from the database.
502 /// This marks all fields as not modified.
503 ///
504 /// @tparam state The target modified state for all fields (Modified or NotModified).
505 /// @tparam Record The record type whose fields are to be updated.
506 /// @param record The record whose field modification flags are set to @p state.
507 template <ModifiedState state, typename Record>
508 void SetModifiedState(Record& record) noexcept;
509
510 /// Loads all direct relations to this record.
511 ///
512 /// @tparam Record The record type whose relation fields are to be populated.
513 /// @param record The record whose BelongsTo, HasMany, HasOneThrough, and HasManyThrough fields are loaded.
514 template <typename Record>
515 void LoadRelations(Record& record);
516
517 /// Configures the auto loading of relations for the given record.
518 ///
519 /// This means, that no explicit loading of relations is required.
520 /// The relations are automatically loaded when accessed.
521 ///
522 /// @tparam Record The record type to configure auto-loading for.
523 /// @param record The record whose relation fields are set up to load lazily on first access.
524 template <typename Record>
525 void ConfigureRelationAutoLoading(Record& record);
526
527 /// Helper function that allow to execute query directly via data mapper
528 /// and get scalar result without need to create SqlStatement manually
529 ///
530 /// @tparam T The scalar type of the expected result value.
531 /// @param sqlQueryString The SQL query string to execute.
532 /// @return The first column of the first result row cast to T, or std::nullopt if the query returns no rows.
533 template <typename T>
534 [[nodiscard]] std::optional<T> Execute(std::string_view sqlQueryString);
535
536 // --------------------------------------------------------------------------------------------
537 // Asynchronous (C++23 coroutine) API.
538 //
539 // Each method offloads its synchronous counterpart to the connection's async backend — a
540 // worker thread, serialized per connection — and resumes the awaiting coroutine on the app's
541 // resume scheduler. Call SqlConnection::EnableAsync(...) on the underlying connection (or use a
542 // pool that stamps it) before invoking any of these. Definitions live in
543 // Async/DataMapperAsync.hpp (included at the end of this header).
544 //
545 // Methods taking a Record& / Record const& capture the record BY REFERENCE, and dereference it
546 // on a worker thread when the returned Task is awaited. The caller must keep the record alive —
547 // and must not mutate or move it — for the entire duration of the co_await (i.e. until the
548 // awaiting coroutine resumes), not merely until the call returns. Destroying, moving, or mutating
549 // it before the co_await resumes is a use-after-free / data race. The idiomatic, safe form keeps
550 // the whole expression in the co_await: `co_await dm.UpdateAsync(record);`.
551
552 /// Asynchronously inserts @p record, updating its primary key in place. @see Create.
553 template <DataMapperOptions QueryOptions = {}, typename Record>
554 [[nodiscard]] Async::Task<RecordPrimaryKeyType<Record>> CreateAsync(Record& record);
555
556 /// Asynchronously queries a single record by its primary key(s). @see QuerySingle.
557 ///
558 /// This is the asynchronous shorthand for a primary-key lookup; for anything else use the fluent
559 /// builder returned by QueryAsync<Record>() (whose finishers also return an Async::Task). Note there
560 /// is deliberately no QueryAsync(string)/QueryAsync(ComposedQuery) — that is what the builder is for.
561 template <typename Record, DataMapperOptions QueryOptions = {}, typename... PrimaryKeyTypes>
562 [[nodiscard]] Async::Task<std::optional<Record>> QuerySingleAsync(PrimaryKeyTypes... primaryKeys);
563
564 /// Asynchronously updates @p record's modified fields. @see Update.
565 template <typename Record>
566 [[nodiscard]] Async::Task<void> UpdateAsync(Record& record);
567
568 /// Asynchronously deletes @p record. @see Delete.
569 template <typename Record>
570 [[nodiscard]] Async::Task<std::size_t> DeleteAsync(Record const& record);
571
572 /// Asynchronously loads @p record's relations. @see LoadRelations.
573 template <typename Record>
574 [[nodiscard]] Async::Task<void> LoadRelationsAsync(Record& record);
575
576 private:
577 /// Builds the comma-separated, fully-qualified (`"Table"."Column"`) field list for @p Record.
578 ///
579 /// Shared by @c Query and @c QueryAsync so the SELECT projection is produced in exactly one place.
580 ///
581 /// @tparam Record The record type whose members are enumerated.
582 /// @return The field list usable as the projection of a SELECT statement.
583 template <typename Record>
584 [[nodiscard]] static std::string BuildFullyQualifiedFieldList()
585 {
586 std::string fields;
587 EnumerateRecordMembers<Record>([&fields]<size_t I, typename Field>() {
588 if (!fields.empty())
589 fields += ", ";
590 fields += '"';
591 fields += RecordTableName<Record>;
592 fields += "\".\"";
593 fields += FieldNameAt<I, Record>;
594 fields += '"';
595 });
596 return fields;
597 }
598
599 /// @brief Queries a single record from the database based on the given query.
600 ///
601 /// @param selectQuery The SQL select query to execute.
602 /// @param args The input parameters for the query.
603 ///
604 /// @return The record if found, otherwise std::nullopt.
605 template <typename Record, typename... Args>
606 std::optional<Record> QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args);
607
608 template <typename Record, typename ValueType>
609 void SetId(Record& record, ValueType&& id);
610
611 template <typename Record, size_t InitialOffset = 1>
612 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
613
614 template <typename ElementMask, typename Record, size_t InitialOffset = 1>
615 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
616
617 template <typename FieldType>
618 std::optional<typename FieldType::ReferencedRecord> LoadBelongsTo(FieldType::ValueType value);
619
620 template <size_t FieldIndex, typename Record, typename OtherRecord>
621 void LoadHasMany(Record& record, HasMany<OtherRecord>& field);
622
623 template <typename ReferencedRecord, typename ThroughRecord, typename Record>
624 void LoadHasOneThrough(Record& record, HasOneThrough<ReferencedRecord, ThroughRecord>& field);
625
626 template <typename ReferencedRecord, typename ThroughRecord, typename Record>
627 void LoadHasManyThrough(Record& record, HasManyThrough<ReferencedRecord, ThroughRecord>& field);
628
629 template <size_t FieldIndex, typename Record, typename OtherRecord, typename Callable>
630 void CallOnHasMany(Record& record, Callable const& callback);
631
632 template <size_t FieldIndex, typename OtherRecord>
633 SqlSelectQueryBuilder BuildHasManySelectQuery();
634
635 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename Callable>
636 void CallOnHasManyThrough(Record& record, Callable const& callback);
637
638 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue, typename Callable>
639 void CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback);
640
641 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue>
642 std::shared_ptr<ReferencedRecord> LoadHasOneThroughByPK(PKValue const& pkValue);
643
644 enum class PrimaryKeySource : std::uint8_t
645 {
646 Record,
647 Override,
648 };
649
650 template <typename Record>
651 std::optional<RecordPrimaryKeyType<Record>> GenerateAutoAssignPrimaryKey(Record const& record);
652
653 template <PrimaryKeySource UsePkOverride, typename Record>
654 RecordPrimaryKeyType<Record> CreateInternal(
655 Record const& record,
656 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
657 pkOverride = std::nullopt);
658
659 SqlConnection _connection;
660 SqlStatement _stmt;
661};
662
663// ------------------------------------------------------------------------------------------------
664
665namespace detail
666{
667 template <typename FieldType>
668 constexpr bool CanSafelyBindOutputColumn(SqlServerType sqlServerType) noexcept
669 {
670 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
671 return true;
672
673 // Test if we have some columns that might not be sufficient to store the result (e.g. string truncation),
674 // then don't call BindOutputColumn but SQLFetch to get the result, because
675 // regrowing previously bound columns is not supported in MS-SQL's ODBC driver, so it seems.
676 bool result = true;
677 if constexpr (IsField<FieldType>)
678 {
679 if constexpr (detail::OneOf<typename FieldType::ValueType,
680 std::string,
681 std::wstring,
682 std::u16string,
683 std::u32string,
684 SqlBinary>
685 || IsSqlDynamicString<typename FieldType::ValueType>
686 || IsSqlDynamicBinary<typename FieldType::ValueType>)
687 {
688 // Known types that MAY require growing due to truncation.
689 result = false;
690 }
691 }
692 return result;
693 }
694
695 template <DataMapperRecord Record>
696 constexpr bool CanSafelyBindOutputColumns(SqlServerType sqlServerType) noexcept
697 {
698 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
699 return true;
700
701 bool result = true;
702 EnumerateRecordMembers<Record>([&result]<size_t I, typename Field>() {
703 if constexpr (IsField<Field>)
704 {
705 if constexpr (detail::OneOf<typename Field::ValueType,
706 std::string,
707 std::wstring,
708 std::u16string,
709 std::u32string,
710 SqlBinary>
711 || IsSqlDynamicString<typename Field::ValueType>
712 || IsSqlDynamicBinary<typename Field::ValueType>)
713 {
714 // Known types that MAY require growing due to truncation.
715 result = false;
716 }
717 }
718 });
719 return result;
720 }
721
722 template <typename Record>
723 void BindAllOutputColumnsWithOffset(SqlResultCursor& reader, Record& record, SQLUSMALLINT startOffset)
724 {
725 EnumerateRecordMembers(record, [reader = &reader, i = startOffset]<size_t I, typename Field>(Field& field) mutable {
726 if constexpr (IsField<Field>)
727 {
728 reader->BindOutputColumn(i++, &field.MutableValue());
729 }
730 else if constexpr (IsBelongsTo<Field>)
731 {
732 reader->BindOutputColumn(i++, &field.MutableValue());
733 }
734 else if constexpr (SqlOutputColumnBinder<Field>)
735 {
736 reader->BindOutputColumn(i++, &field);
737 }
738 });
739 }
740
741 template <typename Record>
742 void BindAllOutputColumns(SqlResultCursor& reader, Record& record)
743 {
744 BindAllOutputColumnsWithOffset(reader, record, 1);
745 }
746
747 /// @brief Requested rows per SQLFetchScroll round-trip for the native row-wise fetch fast path. The
748 /// statement clamps this to a memory budget, so it is an upper bound, not a guarantee.
749 constexpr std::size_t kDefaultRowArrayFetchDepth = 1024;
750
751 /// @brief Mutable-reference output accessor for member @p I that is a Field/BelongsTo: yields the
752 /// field's mutable value so the row-wise fetch path binds the result column in place. The read-side
753 /// counterpart of @ref FieldValueAccessor.
754 template <std::size_t I>
755 struct MutableFieldValueAccessor
756 {
757 template <typename Record>
758 decltype(auto) operator()(Record& record) const
759 {
760 return GetRecordMemberAt<I>(record).MutableValue();
761 }
762 };
763
764 /// @brief The mutable value type bound for member @p FieldType on the row-wise fetch path (the type
765 /// the result column materializes into).
766 template <typename FieldType>
767 using RowWiseColumnValueType = std::remove_cvref_t<decltype(std::declval<FieldType&>().MutableValue())>;
768
769 /// @return Whether @p FieldType maps to a result column on the bound-output path (Field, BelongsTo, or
770 /// a directly-bindable member) — mirrors the classification in @ref BindAllOutputColumnsWithOffset.
771 template <typename FieldType>
772 constexpr bool RowWiseIsColumn()
773 {
774 return IsField<FieldType> || IsBelongsTo<FieldType> || SqlOutputColumnBinder<FieldType>;
775 }
776
777 /// @return Whether @p FieldType is acceptable on the row-wise fetch path: either it is not a result
778 /// column (a relation member, which is not bound) or it is a column whose value type is
779 /// @ref SqlRowWiseFetchableColumn. Directly-bindable non-Field members are conservatively rejected
780 /// (their value would need a separate accessor shape) so such records fall back to the per-row path.
781 template <typename FieldType>
782 constexpr bool RowWiseColumnAcceptable()
783 {
784 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
785 return SqlRowWiseFetchableColumn<RowWiseColumnValueType<FieldType>>;
786 else if constexpr (SqlOutputColumnBinder<FieldType>)
787 return false;
788 else
789 return true; // relation / non-column member: not bound, imposes no constraint
790 }
791
792 template <typename Record, std::size_t... Is>
793 constexpr bool CanRowWiseFetchRecordImpl(std::index_sequence<Is...>)
794 {
795 // The row-strided indicator slots are addressed at i * sizeof(Record); they must stay SQLLEN
796 // aligned, so sizeof(Record) must be a multiple of alignof(SQLLEN) (mirrors the write-side
797 // indicatorAlignmentSatisfied precondition).
798 return (sizeof(Record) % alignof(SQLLEN) == 0) && (RowWiseColumnAcceptable<RecordMemberTypeOf<Is, Record>>() && ...)
799 && (RowWiseIsColumn<RecordMemberTypeOf<Is, Record>>() || ...);
800 }
801
802 /// @brief Whether @p Record can be materialized via the native row-wise array-fetch fast path: every
803 /// result column is a Field/BelongsTo of a @ref SqlRowWiseFetchableColumn type, there is at least one
804 /// column, and the record size keeps the row-strided indicators aligned. Records that fail this fall
805 /// back to the per-row @c SQLFetch path, with identical results.
806 template <typename Record>
807 constexpr bool CanRowWiseFetchRecord()
808 {
809 return CanRowWiseFetchRecordImpl<Record>(std::make_index_sequence<RecordMemberCount<Record>> {});
810 }
811
812 /// Returns a one-element accessor tuple for member @p I when it is a bound result column, else an empty
813 /// tuple — flattened via tuple_cat so the accessor pack matches the bound column set and order exactly.
814 template <std::size_t I, typename Record>
815 auto MakeOutputColumnAccessor()
816 {
817 using FieldType = RecordMemberTypeOf<I, Record>;
818 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
819 return std::tuple<MutableFieldValueAccessor<I>> {};
820 else
821 return std::tuple<> {};
822 }
823
824 /// @brief Materializes the whole result set into @p records via @ref SqlStatement::FetchAllRowWise,
825 /// building one mutable value accessor per bound result column (same set and order as
826 /// @ref BindAllOutputColumnsWithOffset). Precondition: @ref CanRowWiseFetchRecord<Record>().
827 template <typename Record>
828 void ReadAllRowWise(SqlResultCursor& reader, std::vector<Record>* records)
829 {
830 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
831 std::apply(
832 [&](auto const&... accessors) {
833 reader.FetchAllRowWise(*records, kDefaultRowArrayFetchDepth, accessors...);
834 },
835 std::tuple_cat(MakeOutputColumnAccessor<Is, Record>()...));
836 }(std::make_index_sequence<RecordMemberCount<Record>> {});
837 }
838
839 /// @return Whether @p FieldType is a result column whose value is a char fixed-capacity string (or a
840 /// @c std::optional of one). Such columns are array-bound narrow (SQL_C_CHAR), which only round-trips
841 /// byte-exact where @ref SqlConnection::RoundTripsNarrowTextByteExact holds.
842 template <typename FieldType>
843 constexpr bool ColumnIsNarrowFixedString()
844 {
845 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
846 {
847 using V = RowWiseColumnValueType<FieldType>;
848 if constexpr (SqlIsStdOptional<V>)
849 return IsSqlFixedString<typename V::value_type>;
850 else
851 return IsSqlFixedString<V>;
852 }
853 else
854 return false;
855 }
856
857 template <typename Record, std::size_t... Is>
858 constexpr bool RecordHasNarrowFixedStringColumnImpl(std::index_sequence<Is...>)
859 {
860 return (ColumnIsNarrowFixedString<RecordMemberTypeOf<Is, Record>>() || ...);
861 }
862
863 /// @brief Whether @p Record has any char fixed-capacity-string result column. Such records take the
864 /// row-wise fetch fast path only on backends that round-trip narrow text byte-exact; elsewhere they
865 /// fall back to the per-row (wide) path. See @ref SqlConnection::RoundTripsNarrowTextByteExact.
866 template <typename Record>
867 constexpr bool RecordHasNarrowFixedStringColumn()
868 {
869 return RecordHasNarrowFixedStringColumnImpl<Record>(std::make_index_sequence<RecordMemberCount<Record>> {});
870 }
871
872 /// @brief Whether @p Record may use the row-wise fetch fast path on @p serverType: it is row-wise
873 /// fetchable, the driver supports row-array fetch, and any narrow fixed-string column round-trips
874 /// byte-exact there. Single runtime gate composed from connection capabilities + the compile-time
875 /// record shape, so business logic never branches on the server type directly.
876 template <typename Record>
877 bool CanRowWiseFetchOn(SqlServerType serverType)
878 {
879 if constexpr (!CanRowWiseFetchRecord<Record>())
880 return false;
881 else
883 && (!RecordHasNarrowFixedStringColumn<Record>()
885 }
886
887 // --- Two-record tuple (JOIN) fast path ----------------------------------------------------------
888
889 /// @brief Mutable-reference output accessor for member @p I of the @p TupleIndex-th sub-record of a
890 /// @c std::tuple result row; yields that field's mutable value so a JOIN result binds in place.
891 template <std::size_t TupleIndex, std::size_t I>
892 struct MutableTupleFieldAccessor
893 {
894 template <typename TupleType>
895 decltype(auto) operator()(TupleType& row) const
896 {
897 return GetRecordMemberAt<I>(std::get<TupleIndex>(row)).MutableValue();
898 }
899 };
900
901 template <typename First, typename Second, std::size_t... Fs, std::size_t... Ss>
902 constexpr bool CanRowWiseFetchTupleImpl(std::index_sequence<Fs...>, std::index_sequence<Ss...>)
903 {
904 return (sizeof(std::tuple<First, Second>) % alignof(SQLLEN) == 0)
905 && (RowWiseColumnAcceptable<RecordMemberTypeOf<Fs, First>>() && ...)
906 && (RowWiseColumnAcceptable<RecordMemberTypeOf<Ss, Second>>() && ...)
907 && ((RowWiseIsColumn<RecordMemberTypeOf<Fs, First>>() || ...)
908 || (RowWiseIsColumn<RecordMemberTypeOf<Ss, Second>>() || ...));
909 }
910
911 /// @brief Whether a @c std::tuple<First,Second> JOIN row can be materialized via the row-wise fetch
912 /// fast path: both sub-records' columns are row-bindable and the combined row size keeps the
913 /// row-strided indicators aligned.
914 template <typename First, typename Second>
915 constexpr bool CanRowWiseFetchTuple()
916 {
917 return CanRowWiseFetchTupleImpl<First, Second>(std::make_index_sequence<RecordMemberCount<First>> {},
918 std::make_index_sequence<RecordMemberCount<Second>> {});
919 }
920
921 /// @brief Whether a @c std::tuple<First,Second> JOIN row may use the row-wise fetch fast path on
922 /// @p serverType (row-wise fetchable + driver supports row-array fetch + any narrow fixed-string
923 /// column round-trips byte-exact there). The tuple counterpart of @ref CanRowWiseFetchOn.
924 template <typename First, typename Second>
925 bool CanRowWiseFetchTupleOn(SqlServerType serverType)
926 {
927 if constexpr (!CanRowWiseFetchTuple<First, Second>())
928 return false;
929 else
931 && ((!RecordHasNarrowFixedStringColumn<First>() && !RecordHasNarrowFixedStringColumn<Second>())
933 }
934
935 /// Accessor tuple for member @p I of the @p TupleIndex-th sub-record, or empty for non-columns.
936 template <std::size_t TupleIndex, std::size_t I, typename SubRecord>
937 auto MakeTupleColumnAccessor()
938 {
939 using FieldType = RecordMemberTypeOf<I, SubRecord>;
940 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
941 return std::tuple<MutableTupleFieldAccessor<TupleIndex, I>> {};
942 else
943 return std::tuple<> {};
944 }
945
946 /// @brief Materializes a two-record JOIN result set into @p records via row-wise array fetch. The
947 /// accessor pack is First's columns followed by Second's, matching the column order of
948 /// @ref BindAllOutputColumnsWithOffset's offset scheme. Precondition: @ref CanRowWiseFetchTuple.
949 template <typename First, typename Second>
950 void ReadAllRowWiseTuple(SqlResultCursor& reader, std::vector<std::tuple<First, Second>>* records)
951 {
952 [&]<std::size_t... Fs, std::size_t... Ss>(std::index_sequence<Fs...>, std::index_sequence<Ss...>) {
953 std::apply(
954 [&](auto const&... accessors) {
955 reader.FetchAllRowWise(*records, kDefaultRowArrayFetchDepth, accessors...);
956 },
957 std::tuple_cat(MakeTupleColumnAccessor<0, Fs, First>()..., MakeTupleColumnAccessor<1, Ss, Second>()...));
958 }(std::make_index_sequence<RecordMemberCount<First>> {}, std::make_index_sequence<RecordMemberCount<Second>> {});
959 }
960
961 // when we iterate over all columns using element mask
962 // indexes of the mask corresponds to the indexe of the field
963 // inside the structure, not inside the SQL result set
964 template <typename ElementMask, typename Record>
965 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
966 {
967 EnumerateRecordMembers<ElementMask>(
968 record, [reader = &reader, &indexFromQuery]<size_t I, typename Field>(Field& field) mutable {
969 ++indexFromQuery;
970 if constexpr (IsField<Field>)
971 {
972 if constexpr (Field::IsOptional)
973 field.MutableValue() =
974 reader->GetNullableColumn<typename Field::ValueType::value_type>(indexFromQuery);
975 else
976 field.MutableValue() = reader->GetColumn<typename Field::ValueType>(indexFromQuery);
977 }
978 else if constexpr (SqlGetColumnNativeType<Field>)
979 {
980 if constexpr (IsOptionalBelongsTo<Field>)
981 field = reader->GetNullableColumn<typename Field::BaseType>(indexFromQuery);
982 else
983 field = reader->GetColumn<Field>(indexFromQuery);
984 }
985 });
986 }
987
988 template <typename Record>
989 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
990 {
991 return GetAllColumns<std::make_integer_sequence<size_t, RecordMemberCount<Record>>, Record>(
992 reader, record, indexFromQuery);
993 }
994
995 template <typename FirstRecord, typename SecondRecord>
996 // TODO we need to remove this at some points and provide generic bindings for tuples
997 void GetAllColumns(SqlResultCursor& reader, std::tuple<FirstRecord, SecondRecord>& record)
998 {
999 auto& [firstRecord, secondRecord] = record;
1000
1001 EnumerateRecordMembers(firstRecord, [reader = &reader]<size_t I, typename Field>(Field& field) mutable {
1002 if constexpr (IsField<Field>)
1003 {
1004 if constexpr (Field::IsOptional)
1005 field.MutableValue() = reader->GetNullableColumn<typename Field::ValueType::value_type>(I + 1);
1006 else
1007 field.MutableValue() = reader->GetColumn<typename Field::ValueType>(I + 1);
1008 }
1009 else if constexpr (SqlGetColumnNativeType<Field>)
1010 {
1011 if constexpr (Field::IsOptional)
1012 field = reader->GetNullableColumn<typename Field::BaseType>(I + 1);
1013 else
1014 field = reader->GetColumn<Field>(I + 1);
1015 }
1016 });
1017
1018 EnumerateRecordMembers(secondRecord, [reader = &reader]<size_t I, typename Field>(Field& field) mutable {
1019 if constexpr (IsField<Field>)
1020 {
1021 if constexpr (Field::IsOptional)
1022 field.MutableValue() = reader->GetNullableColumn<typename Field::ValueType::value_type>(
1023 RecordMemberCount<FirstRecord> + I + 1);
1024 else
1025 field.MutableValue() =
1026 reader->GetColumn<typename Field::ValueType>(RecordMemberCount<FirstRecord> + I + 1);
1027 }
1028 else if constexpr (SqlGetColumnNativeType<Field>)
1029 {
1030 if constexpr (Field::IsOptional)
1031 field = reader->GetNullableColumn<typename Field::BaseType>(RecordMemberCount<FirstRecord> + I + 1);
1032 else
1033 field = reader->GetColumn<Field>(RecordMemberCount<FirstRecord> + I + 1);
1034 }
1035 });
1036 }
1037
1038 template <typename Record>
1039 bool ReadSingleResult(SqlServerType sqlServerType, SqlResultCursor& reader, Record& record)
1040 {
1041 auto const outputColumnsBound = CanSafelyBindOutputColumns<Record>(sqlServerType);
1042
1043 if (outputColumnsBound)
1044 BindAllOutputColumns(reader, record);
1045
1046 if (!reader.FetchRow())
1047 return false;
1048
1049 if (!outputColumnsBound)
1050 GetAllColumns(reader, record);
1051
1052 return true;
1053 }
1054} // namespace detail
1055
1056template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1057template <typename Finisher>
1058auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RunFinisher(Finisher finisher)
1059{
1060 if constexpr (Derived::QueryExecution == SqlQueryExecutionMode::Asynchronous)
1061 return Async::RunAsync(_dm.Connection().AsyncBackend(), std::move(finisher));
1062 else
1063 return finisher();
1064}
1065
1066template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1068 DataMapper& dm, std::string fields) noexcept:
1069 _dm { dm },
1070 _formatter { dm.Connection().QueryFormatter() },
1071 _fields { std::move(fields) }
1072{
1073 this->_query.searchCondition.inputBindings = &_boundInputs;
1074}
1075
1076template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1077size_t SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::CountImpl()
1078{
1079 auto stmt = SqlStatement { _dm.Connection() };
1080 stmt.Prepare(_formatter.SelectCount(this->_query.distinct,
1081 RecordTableName<Record>,
1082 this->_query.searchCondition.tableAlias,
1083 this->_query.searchCondition.tableJoins,
1084 this->_query.searchCondition.condition));
1085 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1086 if (reader.FetchRow())
1087 return reader.GetColumn<size_t>(1);
1088 return 0;
1089}
1090
1091template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1092bool SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::ExistImpl()
1093{
1094 auto stmt = SqlStatement { _dm.Connection() };
1095
1096 auto const query = _formatter.SelectFirst(this->_query.distinct,
1097 _fields,
1098 RecordTableName<Record>,
1099 this->_query.searchCondition.tableAlias,
1100 this->_query.searchCondition.tableJoins,
1101 this->_query.searchCondition.condition,
1102 this->_query.orderBy,
1103 1);
1104
1105 stmt.Prepare(query);
1106 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
1107 return true;
1108 return false;
1109}
1110
1111template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1112void SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::DeleteImpl()
1113{
1114 auto stmt = SqlStatement { _dm.Connection() };
1115
1116 auto const query = _formatter.Delete(RecordTableName<Record>,
1117 this->_query.searchCondition.tableAlias,
1118 this->_query.searchCondition.tableJoins,
1119 this->_query.searchCondition.condition);
1120
1121 stmt.Prepare(query);
1122 [[maybe_unused]] auto cursor = stmt.ExecuteWithVariants(_boundInputs);
1123}
1124
1125template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1126std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl()
1127{
1128
1129 auto records = std::vector<Record> {};
1130 auto stmt = SqlStatement { _dm.Connection() };
1131 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1132 _fields,
1133 RecordTableName<Record>,
1134 this->_query.searchCondition.tableAlias,
1135 this->_query.searchCondition.tableJoins,
1136 this->_query.searchCondition.condition,
1137 this->_query.orderBy,
1138 this->_query.groupBy));
1139 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1140 if constexpr (DataMapperRecord<Record>)
1141 {
1142 // This can be called when record type is not plain aggregate type
1143 // but more complex tuple, like std::tuple<A, B>
1144 // for now we do not unwrap this type and just skip auto-loading configuration
1145 if constexpr (QueryOptions.loadRelations)
1146 {
1147 for (auto& record: records)
1148 {
1149 _dm.ConfigureRelationAutoLoading(record);
1150 }
1151 }
1152 }
1153 return records;
1154}
1155
1156template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1157template <auto Field>
1158#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1159 requires(is_aggregate_type(parent_of(Field)))
1160#else
1161 requires std::is_member_object_pointer_v<decltype(Field)>
1162#endif
1163auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl() -> std::vector<ReferencedFieldTypeOf<Field>>
1164{
1165 using value_type = ReferencedFieldTypeOf<Field>;
1166 auto result = std::vector<value_type> {};
1167
1168 auto stmt = SqlStatement { _dm.Connection() };
1169 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1170 detail::FullyQualifiedNamesOf<Field>,
1171 RecordTableName<Record>,
1172 this->_query.searchCondition.tableAlias,
1173 this->_query.searchCondition.tableJoins,
1174 this->_query.searchCondition.condition,
1175 this->_query.orderBy,
1176 this->_query.groupBy));
1177 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1178 auto const outputColumnsBound = detail::CanSafelyBindOutputColumn<value_type>(stmt.Connection().ServerType());
1179 while (true)
1180 {
1181 auto& value = result.emplace_back();
1182 if (outputColumnsBound)
1183 reader.BindOutputColumn(1, &value);
1184
1185 if (!reader.FetchRow())
1186 {
1187 result.pop_back();
1188 break;
1189 }
1190
1191 if (!outputColumnsBound)
1192 value = reader.GetColumn<value_type>(1);
1193 }
1194
1195 return result;
1196}
1197
1198template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1199template <auto... ReferencedFields>
1200 requires(sizeof...(ReferencedFields) >= 2)
1201auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl() -> std::vector<Record>
1202{
1203 auto records = std::vector<Record> {};
1204 auto stmt = SqlStatement { _dm.Connection() };
1205
1206 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1207 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1208 RecordTableName<Record>,
1209 this->_query.searchCondition.tableAlias,
1210 this->_query.searchCondition.tableJoins,
1211 this->_query.searchCondition.condition,
1212 this->_query.orderBy,
1213 this->_query.groupBy));
1214
1215 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1216 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1217 while (true)
1218 {
1219 auto& record = records.emplace_back();
1220 if (outputColumnsBound)
1221#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1222 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1223#else
1224 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1225#endif
1226 if (!reader.FetchRow())
1227 {
1228 records.pop_back();
1229 break;
1230 }
1231 if (!outputColumnsBound)
1232 {
1233 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1234 detail::GetAllColumns<ElementMask>(reader, record);
1235 }
1236 }
1237
1238 return records;
1239}
1240
1241template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1242std::optional<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl()
1243{
1244 std::optional<Record> record {};
1245 auto stmt = SqlStatement { _dm.Connection() };
1246 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1247 _fields,
1248 RecordTableName<Record>,
1249 this->_query.searchCondition.tableAlias,
1250 this->_query.searchCondition.tableJoins,
1251 this->_query.searchCondition.condition,
1252 this->_query.orderBy,
1253 1));
1254 Derived::ReadResult(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &record);
1255 if constexpr (QueryOptions.loadRelations)
1256 {
1257 if (record)
1258 _dm.ConfigureRelationAutoLoading(record.value());
1259 }
1260 return record;
1261}
1262
1263template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1264template <auto Field>
1265#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1266 requires(is_aggregate_type(parent_of(Field)))
1267#else
1268 requires std::is_member_object_pointer_v<decltype(Field)>
1269#endif
1270auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl() -> std::optional<ReferencedFieldTypeOf<Field>>
1271{
1272 auto constexpr count = 1;
1273 auto stmt = SqlStatement { _dm.Connection() };
1274 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1275 detail::FullyQualifiedNamesOf<Field>,
1276 RecordTableName<Record>,
1277 this->_query.searchCondition.tableAlias,
1278 this->_query.searchCondition.tableJoins,
1279 this->_query.searchCondition.condition,
1280 this->_query.orderBy,
1281 count));
1282 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
1283 return reader.template GetColumn<ReferencedFieldTypeOf<Field>>(1);
1284 return std::nullopt;
1285}
1286
1287template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1288template <auto... ReferencedFields>
1289 requires(sizeof...(ReferencedFields) >= 2)
1290auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl() -> std::optional<Record>
1291{
1292 auto optionalRecord = std::optional<Record> {};
1293
1294 auto stmt = SqlStatement { _dm.Connection() };
1295 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1296 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1297 RecordTableName<Record>,
1298 this->_query.searchCondition.tableAlias,
1299 this->_query.searchCondition.tableJoins,
1300 this->_query.searchCondition.condition,
1301 this->_query.orderBy,
1302 1));
1303
1304 auto& record = optionalRecord.emplace();
1305 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1306 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1307 if (outputColumnsBound)
1308#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1309 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1310#else
1311 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1312#endif
1313 if (!reader.FetchRow())
1314 return std::nullopt;
1315 if (!outputColumnsBound)
1316 {
1317 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1318 detail::GetAllColumns<ElementMask>(reader, record);
1319 }
1320
1321 if constexpr (QueryOptions.loadRelations)
1322 _dm.ConfigureRelationAutoLoading(record);
1323
1324 return optionalRecord;
1325}
1326
1327template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1328std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl(size_t n)
1329{
1330 auto records = std::vector<Record> {};
1331 auto stmt = SqlStatement { _dm.Connection() };
1332 records.reserve(n);
1333 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1334 _fields,
1335 RecordTableName<Record>,
1336 this->_query.searchCondition.tableAlias,
1337 this->_query.searchCondition.tableJoins,
1338 this->_query.searchCondition.condition,
1339 this->_query.orderBy,
1340 n));
1341 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1342
1343 if constexpr (QueryOptions.loadRelations)
1344 {
1345 for (auto& record: records)
1346 _dm.ConfigureRelationAutoLoading(record);
1347 }
1348 return records;
1349}
1350
1351template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1352std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RangeImpl(size_t offset, size_t limit)
1353{
1354 auto records = std::vector<Record> {};
1355 auto stmt = SqlStatement { _dm.Connection() };
1356 records.reserve(limit);
1357 stmt.Prepare(
1358 _formatter.SelectRange(this->_query.distinct,
1359 _fields,
1360 RecordTableName<Record>,
1361 this->_query.searchCondition.tableAlias,
1362 this->_query.searchCondition.tableJoins,
1363 this->_query.searchCondition.condition,
1364 !this->_query.orderBy.empty()
1365 ? this->_query.orderBy
1366 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1367 this->_query.groupBy,
1368 offset,
1369 limit));
1370 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1371 if constexpr (QueryOptions.loadRelations)
1372 {
1373 for (auto& record: records)
1374 _dm.ConfigureRelationAutoLoading(record);
1375 }
1376 return records;
1377}
1378
1379template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1380template <auto... ReferencedFields>
1381std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RangeImpl(size_t offset, size_t limit)
1382{
1383 auto records = std::vector<Record> {};
1384 auto stmt = SqlStatement { _dm.Connection() };
1385 records.reserve(limit);
1386 stmt.Prepare(
1387 _formatter.SelectRange(this->_query.distinct,
1388 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1389 RecordTableName<Record>,
1390 this->_query.searchCondition.tableAlias,
1391 this->_query.searchCondition.tableJoins,
1392 this->_query.searchCondition.condition,
1393 !this->_query.orderBy.empty()
1394 ? this->_query.orderBy
1395 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1396 this->_query.groupBy,
1397 offset,
1398 limit));
1399
1400 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1401 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1402 while (true)
1403 {
1404 auto& record = records.emplace_back();
1405 if (outputColumnsBound)
1406#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1407 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1408#else
1409 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1410#endif
1411 if (!reader.FetchRow())
1412 {
1413 records.pop_back();
1414 break;
1415 }
1416 if (!outputColumnsBound)
1417 {
1418 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1419 detail::GetAllColumns<ElementMask>(reader, record);
1420 }
1421 }
1422
1423 if constexpr (QueryOptions.loadRelations)
1424 {
1425 for (auto& record: records)
1426 _dm.ConfigureRelationAutoLoading(record);
1427 }
1428
1429 return records;
1430}
1431
1432template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1433template <auto... ReferencedFields>
1434[[nodiscard]] std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl(size_t n)
1435{
1436 auto records = std::vector<Record> {};
1437 auto stmt = SqlStatement { _dm.Connection() };
1438 records.reserve(n);
1439 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1440 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1441 RecordTableName<Record>,
1442 this->_query.searchCondition.tableAlias,
1443 this->_query.searchCondition.tableJoins,
1444 this->_query.searchCondition.condition,
1445 this->_query.orderBy,
1446 n));
1447
1448 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1449 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1450 while (true)
1451 {
1452 auto& record = records.emplace_back();
1453 if (outputColumnsBound)
1454#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1455 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1456#else
1457 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1458#endif
1459 if (!reader.FetchRow())
1460 {
1461 records.pop_back();
1462 break;
1463 }
1464 if (!outputColumnsBound)
1465 {
1466 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1467 detail::GetAllColumns<ElementMask>(reader, record);
1468 }
1469 }
1470
1471 if constexpr (QueryOptions.loadRelations)
1472 {
1473 for (auto& record: records)
1474 _dm.ConfigureRelationAutoLoading(record);
1475 }
1476
1477 return records;
1478}
1479
1480template <typename Record, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1481void SqlAllFieldsQueryBuilder<Record, QueryOptions, Execution>::ReadResults(SqlServerType sqlServerType,
1482 SqlResultCursor reader,
1483 std::vector<Record>* records)
1484{
1485 // Fast path: when every result column is a fixed-width row-bindable field and the driver honours
1486 // native row-array fetching, materialize the whole result set in row blocks (one SQLFetchScroll per
1487 // block) directly into records, instead of one SQLFetch round-trip per row. Results are identical to
1488 // the per-row path below; this only collapses ODBC round-trips (the win on high-latency links).
1489 if constexpr (detail::CanRowWiseFetchRecord<Record>())
1490 {
1491 if (detail::CanRowWiseFetchOn<Record>(sqlServerType))
1492 {
1493 detail::ReadAllRowWise(reader, records);
1494 return;
1495 }
1496 }
1497
1498 while (true)
1499 {
1500 Record& record = records->emplace_back();
1501 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1502 {
1503 records->pop_back();
1504 break;
1505 }
1506 }
1507}
1508
1509template <typename Record, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1510void SqlAllFieldsQueryBuilder<Record, QueryOptions, Execution>::ReadResult(SqlServerType sqlServerType,
1511 SqlResultCursor reader,
1512 std::optional<Record>* optionalRecord)
1513{
1514 Record& record = optionalRecord->emplace();
1515 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1516 optionalRecord->reset();
1517}
1518
1519template <typename FirstRecord, typename SecondRecord, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1520void SqlAllFieldsQueryBuilder<std::tuple<FirstRecord, SecondRecord>, QueryOptions, Execution>::ReadResults(
1521 SqlServerType sqlServerType, SqlResultCursor reader, std::vector<RecordType>* records)
1522{
1523 // Fast path: a JOIN row of two row-bindable records is bound row-wise over the tuple and fetched in
1524 // blocks (one SQLFetchScroll per block) instead of one SQLFetch per row. Identical results.
1525 if constexpr (detail::CanRowWiseFetchTuple<FirstRecord, SecondRecord>())
1526 {
1527 if (detail::CanRowWiseFetchTupleOn<FirstRecord, SecondRecord>(sqlServerType))
1528 {
1529 detail::ReadAllRowWiseTuple<FirstRecord, SecondRecord>(reader, records);
1530 return;
1531 }
1532 }
1533
1534 while (true)
1535 {
1536 auto& record = records->emplace_back();
1537 auto& [firstRecord, secondRecord] = record;
1538
1539 using FirstRecordType = std::remove_cvref_t<decltype(firstRecord)>;
1540 using SecondRecordType = std::remove_cvref_t<decltype(secondRecord)>;
1541
1542 auto const outputColumnsBoundFirst = detail::CanSafelyBindOutputColumns<FirstRecordType>(sqlServerType);
1543 auto const outputColumnsBoundSecond = detail::CanSafelyBindOutputColumns<SecondRecordType>(sqlServerType);
1544 auto const canSafelyBindAll = outputColumnsBoundFirst && outputColumnsBoundSecond;
1545
1546 if (canSafelyBindAll)
1547 {
1548 detail::BindAllOutputColumnsWithOffset(reader, firstRecord, 1);
1549 detail::BindAllOutputColumnsWithOffset(reader, secondRecord, 1 + RecordMemberCount<FirstRecord>);
1550 }
1551
1552 if (!reader.FetchRow())
1553 {
1554 records->pop_back();
1555 break;
1556 }
1557
1558 if (!canSafelyBindAll)
1559 detail::GetAllColumns(reader, record);
1560 }
1561}
1562
1563template <typename Record>
1564std::string DataMapper::Inspect(Record const& record)
1565{
1566 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1567
1568 std::string str;
1569 Reflection::CallOnMembers(record, [&str]<typename Name, typename Value>(Name const& name, Value const& value) {
1570 if (!str.empty())
1571 str += '\n';
1572
1573 if constexpr (FieldWithStorage<Value>)
1574 {
1575 if constexpr (Value::IsOptional)
1576 {
1577 if (!value.Value().has_value())
1578 {
1579 str += std::format("{} {} := <nullopt>", Reflection::TypeNameOf<Value>, name);
1580 }
1581 else
1582 {
1583 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value().value());
1584 }
1585 }
1586 else if constexpr (IsBelongsTo<Value>)
1587 {
1588 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value());
1589 }
1590 else if constexpr (std::same_as<typename Value::ValueType, char>)
1591 {
1592 }
1593 else
1594 {
1595 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.InspectValue());
1596 }
1597 }
1598 else if constexpr (!IsHasMany<Value> && !IsHasManyThrough<Value> && !IsHasOneThrough<Value> && !IsBelongsTo<Value>)
1599 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value);
1600 });
1601 return "{\n" + std::move(str) + "\n}";
1602}
1603
1604template <typename Record>
1605std::vector<std::string> DataMapper::CreateTableString(SqlServerType serverType)
1606{
1607 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1608
1609 auto migration = SqlQueryBuilder(*SqlQueryFormatter::Get(serverType)).Migration();
1610 auto createTable = migration.CreateTable(RecordTableName<Record>);
1611 detail::PopulateCreateTableBuilder<Record>(createTable);
1612 return migration.GetPlan().ToSql();
1613}
1614
1615template <typename FirstRecord, typename... MoreRecords>
1616std::vector<std::string> DataMapper::CreateTablesString(SqlServerType serverType)
1617{
1618 std::vector<std::string> output;
1619 auto const append = [&output](auto const& sql) {
1620 output.insert(output.end(), sql.begin(), sql.end());
1621 };
1622 append(CreateTableString<FirstRecord>(serverType));
1623 (append(CreateTableString<MoreRecords>(serverType)), ...);
1624 return output;
1625}
1626
1627template <typename Record>
1629{
1630 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1631
1632 ZoneScopedN("DataMapper::CreateTable");
1633 ZoneTextObject(RecordTableName<Record>);
1634
1635 auto const sqlQueryStrings = CreateTableString<Record>(_connection.ServerType());
1636 for (auto const& sqlQueryString: sqlQueryStrings) [[maybe_unused]]
1637 auto cursor = _stmt.ExecuteDirect(sqlQueryString);
1638}
1639
1640template <typename FirstRecord, typename... MoreRecords>
1642{
1643 CreateTable<FirstRecord>();
1644 (CreateTable<MoreRecords>(), ...);
1645}
1646
1647template <typename Record>
1648std::optional<RecordPrimaryKeyType<Record>> DataMapper::GenerateAutoAssignPrimaryKey(Record const& record)
1649{
1650 std::optional<RecordPrimaryKeyType<Record>> result;
1652 record, [this, &result]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
1653 if constexpr (IsField<PrimaryKeyType> && IsPrimaryKey<PrimaryKeyType>
1654 && detail::IsAutoAssignPrimaryKeyField<PrimaryKeyType>::value)
1655 {
1656 using ValueType = PrimaryKeyType::ValueType;
1657 if constexpr (std::same_as<ValueType, SqlGuid>)
1658 {
1659 if (!primaryKeyField.Value())
1660 [&](auto& res) {
1661 res.emplace(SqlGuid::Create());
1662 }(result);
1663 }
1664 else if constexpr (requires { ValueType {} + 1; })
1665 {
1666 if (primaryKeyField.Value() == ValueType {})
1667 {
1668 auto maxId = SqlStatement { _connection }.ExecuteDirectScalar<ValueType>(
1669 std::format(R"sql(SELECT MAX("{}") FROM "{}")sql",
1670 FieldNameAt<PrimaryKeyIndex, Record>,
1671 RecordTableName<Record>));
1672 result = maxId.value_or(ValueType {}) + 1;
1673 }
1674 }
1675 }
1676 });
1677 return result;
1678}
1679
1680template <DataMapper::PrimaryKeySource UsePkOverride, typename Record>
1681RecordPrimaryKeyType<Record> DataMapper::CreateInternal(
1682 Record const& record,
1683 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
1684 pkOverride)
1685{
1686 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1687
1688 auto query = _connection.Query(RecordTableName<Record>).Insert(nullptr);
1689
1690#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1691 constexpr auto ctx = std::meta::access_context::current();
1692 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1693 {
1694 using FieldType = typename[:std::meta::type_of(el):];
1695 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1696 query.Set(FieldNameOf<el>, SqlWildcard);
1697 }
1698#else
1699 EnumerateRecordMembers(record, [&query]<auto I, typename FieldType>(FieldType const& /*field*/) {
1700 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1701 query.Set(FieldNameAt<I, Record>, SqlWildcard);
1702 });
1703#endif
1704
1705 _stmt.Prepare(query);
1706
1707#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1708 int i = 1;
1709 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1710 {
1711 using FieldType = typename[:std::meta::type_of(el):];
1712 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1713 {
1714 if constexpr (IsPrimaryKey<FieldType> && UsePkOverride == PrimaryKeySource::Override)
1715 _stmt.BindInputParameter(i++, *pkOverride, std::meta::identifier_of(el));
1716 else
1717 _stmt.BindInputParameter(i++, record.[:el:], std::meta::identifier_of(el));
1718 }
1719 }
1720#else
1721 Reflection::CallOnMembers(record,
1722 [this, &pkOverride, i = SQLSMALLINT { 1 }]<typename Name, typename FieldType>(
1723 Name const& name, FieldType const& field) mutable {
1724 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1725 {
1726 if constexpr (IsPrimaryKey<FieldType> && UsePkOverride == PrimaryKeySource::Override)
1727 _stmt.BindInputParameter(i++, *pkOverride, name);
1728 else
1729 _stmt.BindInputParameter(i++, field, name);
1730 }
1731 });
1732#endif
1733 [[maybe_unused]] auto cursor = _stmt.Execute();
1734
1735 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1736 return { _stmt.LastInsertId(RecordTableName<Record>) };
1737 else if constexpr (HasPrimaryKey<Record>)
1738 {
1739 if constexpr (UsePkOverride == PrimaryKeySource::Override)
1740 return *pkOverride; // NOLINT(bugprone-unchecked-optional-access)
1741 else
1742 return RecordPrimaryKeyOf(record).Value();
1743 }
1744
1745 return {};
1746}
1747
1748template <typename Record>
1749RecordPrimaryKeyType<Record> DataMapper::CreateExplicit(Record const& record)
1750{
1751 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1752 return CreateInternal<PrimaryKeySource::Record>(record);
1753}
1754
1755namespace detail
1756{
1757 /// @brief Whether member field type @p FieldType is an insertable column for a batched CREATE
1758 /// (bindable and not an auto-increment primary key). Single source of truth shared by the INSERT
1759 /// column-list builder and the value-accessor builder, so the bound `?` count and the accessor count
1760 /// cannot drift apart.
1761 template <typename FieldType>
1762 constexpr bool IsBatchInsertColumn = SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>;
1763
1764 /// @brief Whether @p FieldType is a SET column for a batched UPDATE (storable, non-primary-key).
1765 template <typename FieldType>
1766 constexpr bool IsBatchUpdateSetColumn = FieldWithStorage<FieldType> && !IsPrimaryKey<FieldType>;
1767
1768 /// @brief Whether @p FieldType is a WHERE (key) column for a batched UPDATE (a primary key).
1769 template <typename FieldType>
1770 constexpr bool IsBatchUpdateWhereColumn = IsPrimaryKey<FieldType>;
1771
1772 /// @brief Column accessor for batched DataMapper operations: maps a record to the value of its
1773 /// I-th member field, returning a reference so the native row-wise batch path binds it in place.
1774 template <std::size_t I>
1775 struct FieldValueAccessor
1776 {
1777 template <typename Record>
1778 decltype(auto) operator()(Record const& record) const
1779 {
1780 return GetRecordMemberAt<I>(record).Value();
1781 }
1782 };
1783
1784 /// Returns a one-element accessor tuple for member I when it is an insertable column (bindable and
1785 /// not an auto-increment primary key), or an empty tuple otherwise — to be flattened via tuple_cat.
1786 template <std::size_t I, typename Record>
1787 auto MakeCreateColumnAccessor()
1788 {
1789 using FieldType = RecordMemberTypeOf<I, Record>;
1790 if constexpr (IsBatchInsertColumn<FieldType>)
1791 return std::tuple<FieldValueAccessor<I>> {};
1792 else
1793 return std::tuple<> {};
1794 }
1795
1796 /// Accessor tuple for the SET clause of a batched UPDATE: storable, non-primary-key columns.
1797 template <std::size_t I, typename Record>
1798 auto MakeUpdateSetAccessor()
1799 {
1800 using FieldType = RecordMemberTypeOf<I, Record>;
1801 if constexpr (IsBatchUpdateSetColumn<FieldType>)
1802 return std::tuple<FieldValueAccessor<I>> {};
1803 else
1804 return std::tuple<> {};
1805 }
1806
1807 /// Accessor tuple for the WHERE clause of a batched UPDATE: primary-key columns.
1808 template <std::size_t I, typename Record>
1809 auto MakeUpdateWhereAccessor()
1810 {
1811 using FieldType = RecordMemberTypeOf<I, Record>;
1812 if constexpr (IsBatchUpdateWhereColumn<FieldType>)
1813 return std::tuple<FieldValueAccessor<I>> {};
1814 else
1815 return std::tuple<> {};
1816 }
1817} // namespace detail
1818
1819template <std::ranges::range Records>
1820void DataMapper::CreateAll(Records const& records)
1821{
1822 static_assert(std::ranges::contiguous_range<Records> && std::ranges::sized_range<Records>,
1823 "CreateAll requires a contiguous, sized range of records (e.g. std::vector, std::array, "
1824 "std::span, or a C array); native row-wise array binding needs the records laid out contiguously.");
1825 using Record = std::remove_cvref_t<std::ranges::range_value_t<Records>>;
1826 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1827
1828 ZoneScopedN("DataMapper::CreateAll");
1829 ZoneTextObject(RecordTableName<Record>);
1830
1831 if (std::ranges::empty(records))
1832 return;
1833
1834 // Build the INSERT once, with the same column set and order as CreateInternal().
1835 auto query = _connection.Query(RecordTableName<Record>).Insert(nullptr);
1836 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
1837 if constexpr (detail::IsBatchInsertColumn<FieldType>)
1838 query.Set(FieldNameAt<I, Record>, SqlWildcard);
1839 });
1840 _stmt.Prepare(query);
1841
1842 // Build one value accessor per bound column (same filter/order) and submit the whole batch.
1843 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
1844 std::apply([&](auto const&... accessors) { std::ignore = _stmt.ExecuteBatch(records, accessors...); },
1845 std::tuple_cat(detail::MakeCreateColumnAccessor<Is, Record>()...));
1846 }(std::make_index_sequence<RecordMemberCount<Record>> {});
1847}
1848
1849template <DataMapperOptions QueryOptions, typename Record>
1850RecordPrimaryKeyType<Record> DataMapper::CreateCopyOf(Record const& originalRecord)
1851{
1852 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1853 static_assert(HasPrimaryKey<Record>, "CreateCopyOf requires a record type with a primary key");
1854
1855 auto generatedKey = GenerateAutoAssignPrimaryKey(originalRecord);
1856 if (generatedKey)
1857 return CreateInternal<PrimaryKeySource::Override>(originalRecord, generatedKey);
1858
1859 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1860 return CreateInternal<PrimaryKeySource::Record>(originalRecord);
1861
1862 return CreateInternal<PrimaryKeySource::Override>(originalRecord, RecordPrimaryKeyType<Record> {});
1863}
1864
1865template <DataMapperOptions QueryOptions, typename Record>
1866RecordPrimaryKeyType<Record> DataMapper::Create(Record& record)
1867{
1868 static_assert(!std::is_const_v<Record>);
1869 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1870
1871 ZoneScopedN("DataMapper::Create");
1872 ZoneTextObject(RecordTableName<Record>);
1873
1874 auto generatedKey = GenerateAutoAssignPrimaryKey(record);
1875 if (generatedKey)
1876 SetId(record, *generatedKey);
1877
1878 auto pk = CreateInternal<PrimaryKeySource::Record>(record);
1879
1880 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1881 SetId(record, pk);
1882
1883 SetModifiedState<ModifiedState::NotModified>(record);
1884
1885 if constexpr (QueryOptions.loadRelations)
1887
1888 if constexpr (HasPrimaryKey<Record>)
1889 return GetPrimaryKeyField(record);
1890}
1891
1892template <typename Record>
1893bool DataMapper::IsModified(Record const& record) const noexcept
1894{
1895 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1896
1897 bool modified = false;
1898
1899#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1900 auto constexpr ctx = std::meta::access_context::current();
1901 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1902 {
1903 if constexpr (requires { record.[:el:].IsModified(); })
1904 {
1905 modified = modified || record.[:el:].IsModified();
1906 }
1907 }
1908#else
1909 Reflection::CallOnMembers(record, [&modified](auto const& /*name*/, auto const& field) {
1910 if constexpr (requires { field.IsModified(); })
1911 {
1912 modified = modified || field.IsModified();
1913 }
1914 });
1915#endif
1916
1917 return modified;
1918}
1919
1920template <typename Record>
1921void DataMapper::Update(Record& record)
1922{
1923 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1924
1925 ZoneScopedN("DataMapper::Update");
1926 ZoneTextObject(RecordTableName<Record>);
1927
1928 auto query = _connection.Query(RecordTableName<Record>).Update();
1929
1930#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1931 auto constexpr ctx = std::meta::access_context::current();
1932 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1933 {
1934 using FieldType = typename[:std::meta::type_of(el):];
1935 if constexpr (FieldWithStorage<FieldType>)
1936 {
1937 if (record.[:el:].IsModified())
1938 query.Set(FieldNameOf<el>, SqlWildcard);
1939 if constexpr (IsPrimaryKey<FieldType>)
1940 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
1941 }
1942 }
1943#else
1944 EnumerateRecordMembers(record, [&query]<size_t I, typename FieldType>(FieldType const& field) {
1945 if (field.IsModified())
1946 query.Set(FieldNameAt<I, Record>, SqlWildcard);
1947 // for some reason compiler do not want to properly deduce FieldType, so here we
1948 // directly infer the type from the Record type and index
1949 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
1950 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
1951 });
1952#endif
1953 _stmt.Prepare(query);
1954
1955 SQLSMALLINT i = 1;
1956
1957#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1958 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1959 {
1960 if (record.[:el:].IsModified())
1961 {
1962 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
1963 }
1964 }
1965
1966 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1967 {
1968 using FieldType = typename[:std::meta::type_of(el):];
1969 if constexpr (FieldType::IsPrimaryKey)
1970 {
1971 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
1972 }
1973 }
1974#else
1975 // Bind the SET clause
1976 EnumerateRecordMembers(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
1977 if (field.IsModified())
1978 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
1979 });
1980
1981 // Bind the WHERE clause
1982 EnumerateRecordMembers(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
1983 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
1984 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
1985 });
1986#endif
1987
1988 [[maybe_unused]] auto cursor = _stmt.Execute();
1989
1990 SetModifiedState<ModifiedState::NotModified>(record);
1991}
1992
1993template <std::ranges::range Records>
1994void DataMapper::UpdateAll(Records const& records)
1995{
1996 static_assert(std::ranges::contiguous_range<Records> && std::ranges::sized_range<Records>,
1997 "UpdateAll requires a contiguous, sized range of records (e.g. std::vector, std::array, "
1998 "std::span, or a C array); native row-wise array binding needs the records laid out contiguously.");
1999 using Record = std::remove_cvref_t<std::ranges::range_value_t<Records>>;
2000 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2001 static_assert(HasPrimaryKey<Record>, "UpdateAll requires a record type with a primary key");
2002
2003 ZoneScopedN("DataMapper::UpdateAll");
2004 ZoneTextObject(RecordTableName<Record>);
2005
2006 if (std::ranges::empty(records))
2007 return;
2008
2009 // Build one UPDATE that writes all storable non-primary-key columns, matched on the primary key(s).
2010 auto query = _connection.Query(RecordTableName<Record>).Update();
2011 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
2012 if constexpr (detail::IsBatchUpdateSetColumn<FieldType>)
2013 query.Set(FieldNameAt<I, Record>, SqlWildcard);
2014 });
2015 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
2016 if constexpr (detail::IsBatchUpdateWhereColumn<FieldType>)
2017 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
2018 });
2019 _stmt.Prepare(query);
2020
2021 // Accessor order must match the SQL parameter order: SET columns first, then the WHERE key(s).
2022 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
2023 std::apply([&](auto const&... accessors) { std::ignore = _stmt.ExecuteBatch(records, accessors...); },
2024 std::tuple_cat(detail::MakeUpdateSetAccessor<Is, Record>()...,
2025 detail::MakeUpdateWhereAccessor<Is, Record>()...));
2026 }(std::make_index_sequence<RecordMemberCount<Record>> {});
2027}
2028
2029template <typename Record>
2030std::size_t DataMapper::Delete(Record const& record)
2031{
2032 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2033
2034 ZoneScopedN("DataMapper::Delete");
2035 ZoneTextObject(RecordTableName<Record>);
2036
2037 auto query = _connection.Query(RecordTableName<Record>).Delete();
2038
2039#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2040 auto constexpr ctx = std::meta::access_context::current();
2041 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2042 {
2043 using FieldType = typename[:std::meta::type_of(el):];
2044 if constexpr (FieldType::IsPrimaryKey)
2045 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
2046 }
2047#else
2048 EnumerateRecordMembers(record, [&query]<size_t I, typename FieldType>(FieldType const& /*field*/) {
2049 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
2050 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
2051 });
2052#endif
2053
2054 _stmt.Prepare(query);
2055
2056#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2057 SQLSMALLINT i = 1;
2058 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2059 {
2060 using FieldType = typename[:std::meta::type_of(el):];
2061 if constexpr (FieldType::IsPrimaryKey)
2062 {
2063 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
2064 }
2065 }
2066#else
2067 // Bind the WHERE clause
2069 [this, i = SQLSMALLINT { 1 }]<size_t I, typename FieldType>(FieldType const& field) mutable {
2070 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
2071 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
2072 });
2073#endif
2074
2075 auto cursor = _stmt.Execute();
2076
2077 return cursor.NumRowsAffected();
2078}
2079
2080template <typename Record, DataMapperOptions QueryOptions, typename... PrimaryKeyTypes>
2081std::optional<Record> DataMapper::QuerySingle(PrimaryKeyTypes&&... primaryKeys)
2082{
2083 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2084
2085 ZoneScopedN("DataMapper::QuerySingle(PK)");
2086 ZoneTextObject(RecordTableName<Record>);
2087
2088 // Starter doesn't expose finalizers / Where until at least one column is
2089 // projected. The reflection enumeration below is constexpr-conditional, so
2090 // which iteration adds the first column isn't known up front — promote on the
2091 // first storage field via the returned Builder&, then reuse that pointer.
2092 auto selectStarter = _connection.Query(RecordTableName<Record>).Select();
2093 SqlSelectQueryBuilder* queryBuilder = nullptr;
2094 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2095 if constexpr (FieldWithStorage<FieldType>)
2096 {
2097 if (queryBuilder == nullptr)
2098 queryBuilder = &selectStarter.Field(FieldNameAt<I, Record>);
2099 else
2100 queryBuilder->Field(FieldNameAt<I, Record>);
2101
2102 if constexpr (FieldType::IsPrimaryKey)
2103 std::ignore = queryBuilder->Where(FieldNameAt<I, Record>, SqlWildcard);
2104 }
2105 });
2106
2107 _stmt.Prepare(queryBuilder->First());
2108 auto reader = _stmt.Execute(std::forward<PrimaryKeyTypes>(primaryKeys)...);
2109
2110 auto resultRecord = std::optional<Record> { Record {} };
2111 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
2112 return std::nullopt;
2113
2114 if (resultRecord)
2115 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
2116
2117 if constexpr (QueryOptions.loadRelations)
2118 {
2119 if (resultRecord)
2120 ConfigureRelationAutoLoading(*resultRecord);
2121 }
2122
2123 return resultRecord;
2124}
2125
2126template <typename Record, typename... Args>
2127std::optional<Record> DataMapper::QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args)
2128{
2129 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2130
2131 ZoneScopedN("DataMapper::QuerySingle(Builder)");
2132 ZoneTextObject(RecordTableName<Record>);
2133
2134 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2135 if constexpr (FieldWithStorage<FieldType>)
2136 selectQuery.Field(SqlQualifiedTableColumnName { RecordTableName<Record>, FieldNameAt<I, Record> });
2137 });
2138 auto const composedSql = selectQuery.First().ToSql();
2139 ZoneTextObject(composedSql);
2140 _stmt.Prepare(composedSql);
2141 auto reader = _stmt.Execute(std::forward<Args>(args)...);
2142
2143 auto resultRecord = std::optional<Record> { Record {} };
2144 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
2145 return std::nullopt;
2146
2147 if (resultRecord)
2148 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
2149
2150 return resultRecord;
2151}
2152
2153// TODO: Provide Query(QueryBuilder, ...) method variant
2154
2155/// Queries multiple records from the database using a composed query and optional input parameters.
2156template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2157inline LIGHTWEIGHT_FORCE_INLINE std::vector<Record> DataMapper::Query(
2158 SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters)
2159{
2160 static_assert(DataMapperRecord<Record> || std::same_as<Record, SqlVariantRow>, "Record must satisfy DataMapperRecord");
2161
2162 ZoneScopedN("DataMapper::Query(ComposedQuery)");
2163 return Query<Record, QueryOptions>(selectQuery.ToSql(), std::forward<InputParameters>(inputParameters)...);
2164}
2165
2166template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2167std::vector<Record> DataMapper::Query(std::string_view sqlQueryString, InputParameters&&... inputParameters)
2168{
2169 ZoneScopedN("DataMapper::Query(string)");
2170 ZoneTextObject(sqlQueryString);
2171
2172 auto result = std::vector<Record> {};
2173 if constexpr (std::same_as<Record, SqlVariantRow>)
2174 {
2175 _stmt.Prepare(sqlQueryString);
2176 SqlResultCursor cursor = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2177 size_t const numResultColumns = cursor.NumColumnsAffected();
2178 while (cursor.FetchRow())
2179 {
2180 auto& record = result.emplace_back();
2181 record.reserve(numResultColumns);
2182 for (auto const i: std::views::iota(1U, numResultColumns + 1))
2183 record.emplace_back(cursor.GetColumn<SqlVariant>(static_cast<SQLUSMALLINT>(i)));
2184 }
2185 }
2186 else
2187 {
2188 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2189
2190 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
2191
2192 _stmt.Prepare(sqlQueryString);
2193 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2194
2195 for (;;)
2196 {
2197 auto& record = result.emplace_back();
2198
2199 if (canSafelyBindOutputColumns)
2200 BindOutputColumns(record, reader);
2201
2202 if (!reader.FetchRow())
2203 break;
2204
2205 if (!canSafelyBindOutputColumns)
2206 detail::GetAllColumns(reader, record);
2207 }
2208
2209 // Drop the last record, which we failed to fetch (End of result set).
2210 result.pop_back();
2211
2212 for (auto& record: result)
2213 {
2214 SetModifiedState<ModifiedState::NotModified>(record);
2215 if constexpr (QueryOptions.loadRelations)
2217 }
2218 }
2219
2220 return result;
2221}
2222
2223template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions>
2224 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
2225std::vector<std::tuple<First, Second, Rest...>> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery)
2226{
2227 using value_type = std::tuple<First, Second, Rest...>;
2228 auto result = std::vector<value_type> {};
2229
2230 ZoneScopedN("DataMapper::Query(ComposedQuery -> tuple)");
2231 auto const tupleSql = selectQuery.ToSql();
2232 ZoneTextObject(tupleSql);
2233 _stmt.Prepare(tupleSql);
2234 auto reader = _stmt.Execute();
2235
2236 constexpr auto calculateOffset = []<size_t I, typename Tuple>() {
2237 size_t offset = 1;
2238
2239 if constexpr (I > 0)
2240 {
2241 [&]<size_t... Indices>(std::index_sequence<Indices...>) {
2242 ((Indices < I ? (offset += RecordMemberCount<std::tuple_element_t<Indices, Tuple>>) : 0), ...);
2243 }(std::make_index_sequence<I> {});
2244 }
2245 return offset;
2246 };
2247
2248 auto const BindElements = [&](auto& record) {
2249 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2250 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
2251 auto& element = std::get<I>(record);
2252 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
2253 this->BindOutputColumns<TupleElement, offset>(element, reader);
2254 });
2255 };
2256
2257 auto const GetElements = [&](auto& record) {
2258 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2259 auto& element = std::get<I>(record);
2260 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
2261 detail::GetAllColumns(reader, element, offset - 1);
2262 });
2263 };
2264
2265 bool const canSafelyBindOutputColumns = [&]() {
2266 bool result = true;
2267 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2268 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
2269 result &= detail::CanSafelyBindOutputColumns<TupleElement>(_stmt.Connection().ServerType());
2270 });
2271 return result;
2272 }();
2273
2274 for (;;)
2275 {
2276 auto& record = result.emplace_back();
2277
2278 if (canSafelyBindOutputColumns)
2279 BindElements(record);
2280
2281 if (!reader.FetchRow())
2282 break;
2283
2284 if (!canSafelyBindOutputColumns)
2285 GetElements(record);
2286 }
2287
2288 // Drop the last record, which we failed to fetch (End of result set).
2289 result.pop_back();
2290
2291 for (auto& record: result)
2292 {
2293 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2294 auto& element = std::get<I>(record);
2295 SetModifiedState<ModifiedState::NotModified>(element);
2296 if constexpr (QueryOptions.loadRelations)
2297 {
2299 }
2300 });
2301 }
2302
2303 return result;
2304}
2305
2306template <typename ElementMask, typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2307std::vector<Record> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery,
2308 InputParameters&&... inputParameters)
2309{
2310 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2311
2312 ZoneScopedN("DataMapper::Query(ComposedQuery, ElementMask)");
2313 auto const maskedSql = selectQuery.ToSql();
2314 ZoneTextObject(maskedSql);
2315 _stmt.Prepare(maskedSql);
2316
2317 auto records = std::vector<Record> {};
2318
2319 // TODO: We could optimize this further by only considering ElementMask fields in Record.
2320 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
2321
2322 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2323
2324 for (;;)
2325 {
2326 auto& record = records.emplace_back();
2327
2328 if (canSafelyBindOutputColumns)
2329 BindOutputColumns<ElementMask>(record, reader);
2330
2331 if (!reader.FetchRow())
2332 break;
2333
2334 if (!canSafelyBindOutputColumns)
2335 detail::GetAllColumns<ElementMask>(reader, record);
2336 }
2337
2338 // Drop the last record, which we failed to fetch (End of result set).
2339 records.pop_back();
2340
2341 for (auto& record: records)
2342 {
2343 SetModifiedState<ModifiedState::NotModified>(record);
2344 if constexpr (QueryOptions.loadRelations)
2346 }
2347
2348 return records;
2349}
2350
2351template <DataMapper::ModifiedState state, typename Record>
2352void DataMapper::SetModifiedState(Record& record) noexcept
2353{
2354 static_assert(!std::is_const_v<Record>);
2355 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2356
2357 EnumerateRecordMembers(record, []<size_t I, typename FieldType>(FieldType& field) {
2358 if constexpr (requires { field.SetModified(false); })
2359 {
2360 if constexpr (state == ModifiedState::Modified)
2361 field.SetModified(true);
2362 else
2363 field.SetModified(false);
2364 }
2365 });
2366}
2367
2368template <typename Record, typename Callable>
2369inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Record& record, Callable const& callable)
2370{
2371 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2372
2373 EnumerateRecordMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
2374 if constexpr (IsField<FieldType>)
2375 {
2376 if constexpr (FieldType::IsPrimaryKey)
2377 {
2378 return callable.template operator()<I, FieldType>(field);
2379 }
2380 }
2381 });
2382}
2383
2384template <typename Record, typename Callable>
2385inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Callable const& callable)
2386{
2387 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2388
2389 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2390 if constexpr (IsField<FieldType>)
2391 {
2392 if constexpr (FieldType::IsPrimaryKey)
2393 {
2394 return callable.template operator()<I, FieldType>();
2395 }
2396 }
2397 });
2398}
2399
2400template <typename Record, typename Callable>
2401inline LIGHTWEIGHT_FORCE_INLINE void CallOnBelongsTo(Callable const& callable)
2402{
2403 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2404
2405 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2406 if constexpr (IsBelongsTo<FieldType>)
2407 {
2408 return callable.template operator()<I, FieldType>();
2409 }
2410 });
2411}
2412
2413template <typename FieldType>
2414std::optional<typename FieldType::ReferencedRecord> DataMapper::LoadBelongsTo(FieldType::ValueType value)
2415{
2416 using ReferencedRecord = FieldType::ReferencedRecord;
2417
2418 ZoneScopedN("DataMapper::LoadBelongsTo");
2419 ZoneTextObject(RecordTableName<ReferencedRecord>);
2420
2421 std::optional<ReferencedRecord> record { std::nullopt };
2422
2423#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2424 auto constexpr ctx = std::meta::access_context::current();
2425 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^ReferencedRecord, ctx)))
2426 {
2427 using BelongsToFieldType = typename[:std::meta::type_of(el):];
2428 if constexpr (IsField<BelongsToFieldType>)
2429 if constexpr (BelongsToFieldType::IsPrimaryKey)
2430 {
2431 if (auto result = QuerySingle<ReferencedRecord>(value); result)
2432 record = std::move(result);
2433 else
2435 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
2436 }
2437 }
2438#else
2439 CallOnPrimaryKey<ReferencedRecord>([&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>() {
2440 if (auto result = QuerySingle<ReferencedRecord>(value); result)
2441 record = std::move(result);
2442 else
2444 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
2445 });
2446#endif
2447 return record;
2448}
2449
2450template <size_t FieldIndex, typename Record, typename OtherRecord, typename Callable>
2451void DataMapper::CallOnHasMany(Record& record, Callable const& callback)
2452{
2453 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2454 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
2455
2456 using FieldType = HasMany<OtherRecord>;
2457 using ReferencedRecord = FieldType::ReferencedRecord;
2458
2459 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2460 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2461 .Select()
2462 .Build([&](auto& query) {
2463 EnumerateRecordMembers<ReferencedRecord>(
2464 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2465 if constexpr (FieldWithStorage<ReferencedFieldType>)
2466 {
2467 query.Field(FieldNameAt<ReferencedFieldIndex, ReferencedRecord>);
2468 }
2469 });
2470 })
2471 .Where(FieldNameAt<FieldIndex, ReferencedRecord>, SqlWildcard)
2472 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord>);
2473 callback(query, primaryKeyField);
2474 });
2475}
2476
2477template <size_t FieldIndex, typename OtherRecord>
2478SqlSelectQueryBuilder DataMapper::BuildHasManySelectQuery()
2479{
2480 return _connection.Query(RecordTableName<OtherRecord>)
2481 .Select()
2482 .Build([](auto& q) {
2483 EnumerateRecordMembers<OtherRecord>([&]<size_t I, typename F>() {
2484 if constexpr (FieldWithStorage<F>)
2485 q.Field(FieldNameAt<I, OtherRecord>);
2486 });
2487 })
2488 .Where(FieldNameAt<FieldIndex, OtherRecord>, SqlWildcard)
2489 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<OtherRecord>, OtherRecord>);
2490}
2491
2492template <size_t FieldIndex, typename Record, typename OtherRecord>
2493void DataMapper::LoadHasMany(Record& record, HasMany<OtherRecord>& field)
2494{
2495 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2496 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
2497
2498 ZoneScopedN("DataMapper::LoadHasMany");
2499 ZoneTextObject(RecordTableName<OtherRecord>);
2500
2501 CallOnHasMany<FieldIndex, Record, OtherRecord>(record, [&](SqlSelectQueryBuilder selectQuery, auto& primaryKeyField) {
2502 field.Emplace(detail::ToSharedPtrList(Query<OtherRecord>(selectQuery.All(), primaryKeyField.Value())));
2503 });
2504}
2505
2506template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2507void DataMapper::LoadHasOneThrough(Record& record, HasOneThrough<ReferencedRecord, ThroughRecord>& field)
2508{
2509 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2510 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2511
2512 ZoneScopedN("DataMapper::LoadHasOneThrough");
2513 ZoneTextObject(RecordTableName<ReferencedRecord>);
2514
2515 // Find the PK of Record
2516 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2517 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2518 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2519 // Find the PK of ThroughRecord
2520 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2521 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2522 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2523 // Query the ReferencedRecord where:
2524 // - the BelongsTo of ReferencedRecord points to the PK of ThroughRecord,
2525 // - and the BelongsTo of ThroughRecord points to the PK of Record
2526 auto query =
2527 _connection.Query(RecordTableName<ReferencedRecord>)
2528 .Select()
2529 .Build([&](auto& query) {
2530 EnumerateRecordMembers<ReferencedRecord>(
2531 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2532 if constexpr (FieldWithStorage<ReferencedFieldType>)
2533 {
2534 query.Field(SqlQualifiedTableColumnName {
2535 RecordTableName<ReferencedRecord>,
2536 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2537 }
2538 });
2539 })
2540 .InnerJoin(RecordTableName<ThroughRecord>,
2541 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2542 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2543 .InnerJoin(RecordTableName<Record>,
2544 FieldNameAt<PrimaryKeyIndex, Record>,
2545 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2546 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2547 .Where(
2548 SqlQualifiedTableColumnName {
2549 RecordTableName<Record>,
2550 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2551 },
2552 SqlWildcard);
2553 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), primaryKeyField.Value()); link)
2554 {
2555 field.EmplaceRecord(std::make_shared<ReferencedRecord>(std::move(*link)));
2556 }
2557 });
2558 });
2559 });
2560 });
2561}
2562
2563template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue>
2564std::shared_ptr<ReferencedRecord> DataMapper::LoadHasOneThroughByPK(PKValue const& pkValue)
2565{
2566 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2567
2568 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2569 std::shared_ptr<ReferencedRecord> result;
2570
2571 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2572 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2573 // Find the PK of ThroughRecord
2574 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2575 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2576 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2577 auto query =
2578 _connection.Query(RecordTableName<ReferencedRecord>)
2579 .Select()
2580 .Build([&](auto& query) {
2581 EnumerateRecordMembers<ReferencedRecord>(
2582 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2583 if constexpr (FieldWithStorage<ReferencedFieldType>)
2584 {
2585 query.Field(SqlQualifiedTableColumnName {
2586 RecordTableName<ReferencedRecord>,
2587 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2588 }
2589 });
2590 })
2591 .InnerJoin(RecordTableName<ThroughRecord>,
2592 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2593 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2594 .InnerJoin(RecordTableName<Record>,
2595 FieldNameAt<PrimaryKeyIndex, Record>,
2596 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2597 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2598 .Where(
2599 SqlQualifiedTableColumnName {
2600 RecordTableName<Record>,
2601 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2602 },
2603 SqlWildcard);
2604 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), pkValue); link)
2605 result = std::make_shared<ReferencedRecord>(std::move(*link));
2606 });
2607 });
2608 });
2609
2610 return result;
2611}
2612
2613template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename Callable>
2614void DataMapper::CallOnHasManyThrough(Record& record, Callable const& callback)
2615{
2616 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2617
2618 // Find the PK of Record
2619 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2620 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2621 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2622 using ThroughBelongsToRecordFieldType = RecordMemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2623 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2624 {
2625 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2626 CallOnBelongsTo<ThroughRecord>(
2627 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2628 using ThroughBelongsToReferenceRecordFieldType =
2629 RecordMemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2630 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2631 ReferencedRecord>)
2632 {
2633 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2634 .Select()
2635 .Build([&](auto& query) {
2636 EnumerateRecordMembers<ReferencedRecord>(
2637 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2638 if constexpr (FieldWithStorage<ReferencedFieldType>)
2639 {
2640 query.Field(SqlQualifiedTableColumnName {
2641 RecordTableName<ReferencedRecord>,
2642 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2643 }
2644 });
2645 })
2646 .InnerJoin(RecordTableName<ThroughRecord>,
2647 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2648 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2649 FieldNameAt<PrimaryKeyIndex, Record> })
2650 .Where(
2651 SqlQualifiedTableColumnName {
2652 RecordTableName<ThroughRecord>,
2653 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2654 },
2655 SqlWildcard);
2656 callback(query, primaryKeyField);
2657 }
2658 });
2659 }
2660 });
2661 });
2662}
2663
2664template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue, typename Callable>
2665void DataMapper::CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback)
2666{
2667 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2668
2669 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2670
2671 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2672 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2673 using ThroughBelongsToRecordFieldType = RecordMemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2674 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2675 {
2676 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2677 CallOnBelongsTo<ThroughRecord>(
2678 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2679 using ThroughBelongsToReferenceRecordFieldType =
2680 RecordMemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2681 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2682 ReferencedRecord>)
2683 {
2684 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2685 .Select()
2686 .Build([&](auto& query) {
2687 EnumerateRecordMembers<ReferencedRecord>(
2688 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2689 if constexpr (FieldWithStorage<ReferencedFieldType>)
2690 {
2691 query.Field(SqlQualifiedTableColumnName {
2692 RecordTableName<ReferencedRecord>,
2693 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2694 }
2695 });
2696 })
2697 .InnerJoin(RecordTableName<ThroughRecord>,
2698 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2699 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2700 FieldNameAt<PrimaryKeyIndex, Record> })
2701 .Where(
2702 SqlQualifiedTableColumnName {
2703 RecordTableName<ThroughRecord>,
2704 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2705 },
2706 SqlWildcard);
2707 callback(query, pkValue);
2708 }
2709 });
2710 }
2711 });
2712}
2713
2714template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2715void DataMapper::LoadHasManyThrough(Record& record, HasManyThrough<ReferencedRecord, ThroughRecord>& field)
2716{
2717 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2718
2719 ZoneScopedN("DataMapper::LoadHasManyThrough");
2720 ZoneTextObject(RecordTableName<ReferencedRecord>);
2721
2722 CallOnHasManyThrough<ReferencedRecord, ThroughRecord>(
2723 record, [&](SqlSelectQueryBuilder& selectQuery, auto& primaryKeyField) {
2724 field.Emplace(detail::ToSharedPtrList(Query<ReferencedRecord>(selectQuery.All(), primaryKeyField.Value())));
2725 });
2726}
2727
2728template <typename Record>
2729void DataMapper::LoadRelations(Record& record)
2730{
2731 static_assert(!std::is_const_v<Record>);
2732 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2733
2734 ZoneScopedN("DataMapper::LoadRelations");
2735 ZoneTextObject(RecordTableName<Record>);
2736
2737#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2738 constexpr auto ctx = std::meta::access_context::current();
2739 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2740 {
2741 using FieldType = typename[:std::meta::type_of(el):];
2742 if constexpr (IsBelongsTo<FieldType>)
2743 {
2744 auto& field = record.[:el:];
2745 field = LoadBelongsTo<FieldType>(field.Value());
2746 }
2747 else if constexpr (IsHasMany<FieldType>)
2748 {
2749 LoadHasMany<el>(record, record.[:el:]);
2750 }
2751 else if constexpr (IsHasOneThrough<FieldType>)
2752 {
2753 LoadHasOneThrough(record, record.[:el:]);
2754 }
2755 else if constexpr (IsHasManyThrough<FieldType>)
2756 {
2757 LoadHasManyThrough(record, record.[:el:]);
2758 }
2759 }
2760#else
2761 EnumerateRecordMembers(record, [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2762 if constexpr (IsBelongsTo<FieldType>)
2763 {
2764 field = LoadBelongsTo<FieldType>(field.Value());
2765 }
2766 else if constexpr (IsHasMany<FieldType>)
2767 {
2768 LoadHasMany<FieldIndex>(record, field);
2769 }
2770 else if constexpr (IsHasOneThrough<FieldType>)
2771 {
2772 LoadHasOneThrough(record, field);
2773 }
2774 else if constexpr (IsHasManyThrough<FieldType>)
2775 {
2776 LoadHasManyThrough(record, field);
2777 }
2778 });
2779#endif
2780}
2781
2782/// Sets the primary key field(s) of the given record to the specified id value.
2783template <typename Record, typename ValueType>
2784inline LIGHTWEIGHT_FORCE_INLINE void DataMapper::SetId(Record& record, ValueType&& id)
2785{
2786 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2787 // static_assert(HasPrimaryKey<Record>);
2788
2789#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2790
2791 auto constexpr ctx = std::meta::access_context::current();
2792 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2793 {
2794 using FieldType = typename[:std::meta::type_of(el):];
2795 if constexpr (IsField<FieldType>)
2796 {
2797 if constexpr (FieldType::IsPrimaryKey)
2798 {
2799 record.[:el:] = std::forward<ValueType>(id);
2800 }
2801 }
2802 }
2803#else
2804 EnumerateRecordMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
2805 if constexpr (IsField<FieldType>)
2806 {
2807 if constexpr (FieldType::IsPrimaryKey)
2808 {
2809 field = std::forward<FieldType>(id);
2810 }
2811 }
2812 });
2813#endif
2814}
2815
2816/// Binds all output columns of the record via the given cursor.
2817template <typename Record, size_t InitialOffset>
2818inline LIGHTWEIGHT_FORCE_INLINE Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2819{
2820 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2821 return BindOutputColumns<std::make_integer_sequence<size_t, RecordMemberCount<Record>>, Record, InitialOffset>(record,
2822 cursor);
2823}
2824
2825template <typename ElementMask, typename Record, size_t InitialOffset>
2826Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2827{
2828 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2829 static_assert(!std::is_const_v<Record>);
2830
2831#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2832 auto constexpr ctx = std::meta::access_context::current();
2833 SQLSMALLINT i = SQLSMALLINT { InitialOffset };
2834 template for (constexpr auto index: define_static_array(template_arguments_of(^^ElementMask)) | std::views::drop(1))
2835 {
2836 constexpr auto el = nonstatic_data_members_of(^^Record, ctx)[[:index:]];
2837 using FieldType = typename[:std::meta::type_of(el):];
2838 if constexpr (IsField<FieldType>)
2839 {
2840 cursor.BindOutputColumn(i++, &record.[:el:].MutableValue());
2841 }
2842 else if constexpr (SqlOutputColumnBinder<FieldType>)
2843 {
2844 cursor.BindOutputColumn(i++, &record.[:el:]);
2845 }
2846 }
2847#else
2848 EnumerateRecordMembers<ElementMask>(
2849 record, [&cursor, i = SQLUSMALLINT { InitialOffset }]<size_t I, typename Field>(Field& field) mutable {
2850 if constexpr (IsField<Field>)
2851 {
2852 cursor.BindOutputColumn(i++, &field.MutableValue());
2853 }
2854 else if constexpr (SqlOutputColumnBinder<Field>)
2855 {
2856 cursor.BindOutputColumn(i++, &field);
2857 }
2858 });
2859#endif
2860
2861 return record;
2862}
2863template <typename Record>
2864// NOLINTNEXTLINE(readability-function-cognitive-complexity)
2866{
2867 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2868
2869 auto const callback = [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2870 if constexpr (IsBelongsTo<FieldType>)
2871 {
2872 field.SetAutoLoader(typename FieldType::Loader {
2873 .loadReference = [value = field.Value()]() -> std::optional<typename FieldType::ReferencedRecord> {
2875 return dm.LoadBelongsTo<FieldType>(value);
2876 },
2877 });
2878 }
2879 if constexpr (IsHasMany<FieldType>)
2880 {
2881 if constexpr (HasPrimaryKey<Record>)
2882 {
2883 using ReferencedRecord = FieldType::ReferencedRecord;
2884 HasMany<ReferencedRecord>& hasMany = field;
2885 // Capture the PK value by value to avoid dangling references if the record is moved.
2886 auto pkValue = GetPrimaryKeyField(record);
2887 hasMany.SetAutoLoader(typename FieldType::Loader {
2888 .count = [pkValue]() -> size_t {
2890 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2891 dm._stmt.Prepare(selectQuery.Count());
2892 SqlResultCursor cursor = dm._stmt.Execute(pkValue);
2893 size_t count = 0;
2894 if (cursor.FetchRow())
2895 count = cursor.GetColumn<size_t>(1);
2896 return count;
2897 },
2898 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2900 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2901 return detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pkValue));
2902 },
2903 .each =
2904 [pkValue](auto const& each) {
2906 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2907 auto stmt = SqlStatement { dm._connection };
2908 stmt.Prepare(selectQuery.All());
2909 auto cursor = stmt.Execute(pkValue);
2910
2911 auto referencedRecord = ReferencedRecord {};
2912 dm.BindOutputColumns(referencedRecord, cursor);
2913 dm.ConfigureRelationAutoLoading(referencedRecord);
2914
2915 while (cursor.FetchRow())
2916 {
2917 each(referencedRecord);
2918 dm.BindOutputColumns(referencedRecord, cursor);
2919 }
2920 },
2921 });
2922 }
2923 }
2924 if constexpr (IsHasOneThrough<FieldType> && HasPrimaryKey<Record>)
2925 {
2926 using ReferencedRecord = FieldType::ReferencedRecord;
2927 using ThroughRecord = FieldType::ThroughRecord;
2929 // Capture the PK value by value to avoid dangling references if the record is moved.
2930 auto pkValue = GetPrimaryKeyField(record);
2931 hasOneThrough.SetAutoLoader(typename FieldType::Loader {
2932 .loadReference = [pkValue]() -> std::shared_ptr<ReferencedRecord> {
2934 return dm.LoadHasOneThroughByPK<ReferencedRecord, ThroughRecord, Record>(pkValue);
2935 },
2936 });
2937 }
2938 if constexpr (IsHasManyThrough<FieldType> && HasPrimaryKey<Record>)
2939 {
2940 using ReferencedRecord = FieldType::ReferencedRecord;
2941 using ThroughRecord = FieldType::ThroughRecord;
2943 // Capture the PK value by value to avoid dangling references if the record is moved.
2944 auto pkValue = GetPrimaryKeyField(record);
2945 hasManyThrough.SetAutoLoader(typename FieldType::Loader {
2946 .count = [pkValue]() -> size_t {
2947 // Load result for Count()
2948 size_t count = 0;
2950 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2951 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2952 dm._stmt.Prepare(selectQuery.Count());
2953 SqlResultCursor cursor = dm._stmt.Execute(pk);
2954 if (cursor.FetchRow())
2955 count = cursor.GetColumn<size_t>(1);
2956 });
2957 return count;
2958 },
2959 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2960 // Load result for All()
2962 typename FieldType::ReferencedRecordList result;
2963 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2964 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2965 result = detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pk));
2966 });
2967 return result;
2968 },
2969 .each =
2970 [pkValue](auto const& each) {
2971 // Load result for Each()
2973 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2974 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2975 auto stmt = SqlStatement { dm._connection };
2976 stmt.Prepare(selectQuery.All());
2977 auto cursor = stmt.Execute(pk);
2978 auto referencedRecord = ReferencedRecord {};
2979 dm.BindOutputColumns(referencedRecord, cursor);
2980 dm.ConfigureRelationAutoLoading(referencedRecord);
2981
2982 while (cursor.FetchRow())
2983 {
2984 each(referencedRecord);
2985 dm.BindOutputColumns(referencedRecord, cursor);
2986 }
2987 });
2988 },
2989 });
2990 }
2991 };
2992
2993#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2994 constexpr auto ctx = std::meta::access_context::current();
2995
2996 Reflection::template_for<0, nonstatic_data_members_of(^^Record, ctx).size()>([&callback, &record]<auto I>() {
2997 constexpr auto localctx = std::meta::access_context::current();
2998 constexpr auto members = define_static_array(nonstatic_data_members_of(^^Record, localctx));
2999 using FieldType = typename[:std::meta::type_of(members[I]):];
3000 callback.template operator()<I, FieldType>(record.[:members[I]:]);
3001 });
3002#else
3003 EnumerateRecordMembers(record, callback);
3004#endif
3005}
3006
3007template <typename T>
3008std::optional<T> DataMapper::Execute(std::string_view sqlQueryString)
3009{
3010 ZoneScopedN("DataMapper::Execute(string)");
3011 ZoneTextObject(sqlQueryString);
3012 return _stmt.ExecuteDirectScalar<T>(sqlQueryString);
3013}
3014
3015} // namespace Lightweight
3016
3017#include "../Async/DataMapperAsync.hpp"
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
Async::Task< void > LoadRelationsAsync(Record &record)
Asynchronously loads record's relations.
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
void UpdateAll(Records const &records)
Batch-updates a span of records with a single prepared statement.
Async::Task< std::optional< Record > > QuerySingleAsync(PrimaryKeyTypes... primaryKeys)
Async::Task< void > UpdateAsync(Record &record)
Asynchronously updates record's modified fields.
std::optional< T > Execute(std::string_view sqlQueryString)
DataMapper(std::optional< SqlConnectionString > connectionString)
Constructs a new data mapper, using the given connection string.
void CreateAll(Records const &records)
Batch-inserts a span of records with a single prepared statement.
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.
Async::Task< RecordPrimaryKeyType< Record > > CreateAsync(Record &record)
Asynchronously inserts record, updating its primary key in place.
void ConfigureRelationAutoLoading(Record &record)
SqlAllFieldsQueryBuilder< Record, QueryOptions, SqlQueryExecutionMode::Asynchronous > QueryAsync()
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.
Async::Task< std::size_t > DeleteAsync(Record const &record)
Asynchronously deletes record.
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
static bool RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept
Whether serverType's driver round-trips narrow (SQL_C_CHAR) character data byte-exact,...
SqlQueryFormatter const & QueryFormatter() const noexcept
Retrieves a query formatter suitable for the SQL server being connected.
bool SupportsNativeRowArrayFetch() const noexcept
Whether this connection's ODBC driver supports native row-array fetching (SQL_ATTR_ROW_ARRAY_SIZE > 1...
LIGHTWEIGHT_FORCE_INLINE SqlCoreDataMapperQueryBuilder(DataMapper &dm, std::string fields) noexcept
Constructs a query builder with the given data mapper and field list.
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:93
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.
SqlResultCursor ExecuteBatch(FirstColumnBatch const &firstColumnBatch, MoreColumnBatches const &... moreColumnBatches)
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:261
constexpr void EnumerateRecordMembers(Record &record, Callable &&callable)
Invokes callable as callable<I>(member) for each member of record.
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:318
Represents a value that can be any of the supported SQL data types.