Lightweight 0.20261031.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 "CompositeForeignKey.hpp"
14#include "Field.hpp"
15#include "HasMany.hpp"
16#include "HasManyThrough.hpp"
17#include "HasOneThrough.hpp"
18#include "QueryBuilders.hpp"
19#include "Record.hpp"
20#include "RelationLoadSource.hpp"
21
22#include <reflection-cpp/reflection.hpp>
23
24#include <cassert>
25#include <chrono>
26#include <concepts>
27#include <exception>
28#include <expected>
29#include <format>
30#include <functional>
31#include <memory>
32#include <optional>
33#include <ranges>
34#include <span>
35#include <tuple>
36#include <type_traits>
37#include <utility>
38#include <vector>
39
40namespace Lightweight
41{
42
43/// @defgroup DataMapper Data Mapper
44///
45/// @brief The data mapper is a high level API for mapping records to and from the database using high level C++ syntax.
46
47namespace detail
48{
49 /// The first element of a non-empty pack of relation members, i.e. the head of a relation path.
50 ///
51 /// Declared up here because `With<>()` names it, and a non-dependent name inside a template is
52 /// looked up where the template is defined, not where it is instantiated.
53 template <auto First, auto...>
54 inline constexpr auto FirstOf = First;
55
56 // Converts a container of T to a container of std::shared_ptr<T>.
57 template <template <typename> class Allocator, template <typename, typename> class Container, typename Object>
58 auto ToSharedPtrList(Container<Object, Allocator<Object>> container)
59 {
60 using SharedPtrRecord = std::shared_ptr<Object>;
61 auto sharedPtrContainer = Container<SharedPtrRecord, Allocator<SharedPtrRecord>> {};
62 for (auto& object: container)
63 sharedPtrContainer.emplace_back(std::make_shared<Object>(std::move(object)));
64 return sharedPtrContainer;
65 }
66} // namespace detail
67
68/// @brief Main API for mapping records to and from the database using high level C++ syntax.
69///
70/// A DataMapper instances operates on a single SQL connection and provides methods to
71/// create, read, update and delete records in the database.
72///
73/// @see Field, BelongsTo, HasMany, HasManyThrough, HasOneThrough
74/// @ingroup DataMapper
75///
76/// @code
77/// struct Person
78/// {
79/// Field<SqlGuid, PrimaryKey::AutoAssign> id;
80/// Field<SqlAnsiString<30>> name;
81/// Field<SqlAnsiString<40>> email;
82/// };
83///
84/// auto dm = DataMapper {};
85///
86/// // Create a new person record
87/// auto person = Person { .id = SqlGuid::Create(), .name = "John Doe", .email = "johnt@doe.com" };
88///
89/// // Create the record in the database and set the primary key on the record
90/// auto const personId = dm.Create(person);
91///
92/// // Query the person record from the database
93/// auto const queriedPerson = dm.Query<Person>(personId)
94/// .Where(FieldNameOf<&Person::id>, "=", personId)
95/// .First();
96///
97/// if (queriedPerson.has_value())
98/// std::println("Queried Person: {}", DataMapper::Inspect(queriedPerson.value()));
99///
100/// // Update the person record in the database
101/// person.email = "alt@doe.com";
102/// dm.Update(person);
103///
104/// // Delete the person record from the database
105/// dm.Delete(person);
106/// @endcode
108{
109 public:
110 /// Constructs a new data mapper, using the default connection.
112 _connection {},
113 _stmt { _connection }
114 {
115 }
116
117 /// Constructs a new data mapper, using the given connection.
118 explicit DataMapper(SqlConnection&& connection):
119 _connection { std::move(connection) },
120 _stmt { _connection }
121 {
122 }
123
124 /// Constructs a new data mapper, using the given connection string.
125 explicit DataMapper(std::optional<SqlConnectionString> connectionString):
126 _connection { std::move(connectionString) },
127 _stmt { _connection }
128 {
129 }
130
131 DataMapper(DataMapper const&) = delete;
132 DataMapper& operator=(DataMapper const&) = delete;
133
134 /// Move constructor.
135 DataMapper(DataMapper&& other) noexcept:
136 _connection(std::move(other._connection)),
137 _stmt(_connection),
138 _relationLoadSource(std::move(other._relationLoadSource)),
139 _timestampClock(std::move(other._timestampClock))
140 {
141 other._stmt = SqlStatement(std::nullopt);
142 }
143
144 /// Move assignment operator.
145 DataMapper& operator=(DataMapper&& other) noexcept
146 {
147 if (this == &other)
148 return *this;
149
150 _connection = std::move(other._connection);
151 _stmt = SqlStatement(_connection);
152 other._stmt = SqlStatement(std::nullopt);
153 _relationLoadSource = std::move(other._relationLoadSource);
154 _timestampClock = std::move(other._timestampClock);
155
156 return *this;
157 }
158
159 ~DataMapper() = default;
160
161 /// Returns the connection reference used by this data mapper.
162 [[nodiscard]] SqlConnection const& Connection() const noexcept
163 {
164 return _connection;
165 }
166
167 /// Returns the mutable connection reference used by this data mapper.
168 [[nodiscard]] SqlConnection& Connection() noexcept
169 {
170 return _connection;
171 }
172
173 /// A source of the current time, used to fill fields marked with a @ref FieldTimestamp.
174 using TimestampClock = std::function<SqlDateTime()>;
175
176 /// @brief Sets the clock that fills the auto-maintained timestamp fields (see @ref FieldTimestamp).
177 ///
178 /// The clock is the client's: the time is taken here, not by the database server. It is read once
179 /// per write operation, so every timestamp written by one Create(), Update(), CreateAll(), ... carries
180 /// the same value. Asynchronous methods read it on the worker thread that runs the operation.
181 ///
182 /// @param clock The clock to use. An empty function restores the default, the system clock in UTC.
184 {
185 _timestampClock = std::move(clock);
186 }
187
188 /// @brief Reads the clock that fills the auto-maintained timestamp fields.
189 ///
190 /// @return The time from the clock set by SetTimestampClock(), or the current UTC system time if none is set.
191 [[nodiscard]] SqlDateTime CurrentTimestamp() const
192 {
193 if (_timestampClock)
194 return _timestampClock();
195 // std::chrono::system_clock is Unix time, i.e. UTC (see SqlDateTime::NowUTC()).
196 return SqlDateTime { std::chrono::system_clock::now() };
197 }
198
199#if defined(BUILD_TESTS)
200
201 [[nodiscard]] SqlStatement& Statement(this auto&& self) noexcept
202 {
203 return self._stmt;
204 }
205
206#endif
207
208 /// Constructs a human readable string representation of the given record.
209 template <typename Record>
210 static std::string Inspect(Record const& record);
211
212 /// Constructs a string list of SQL queries to create the table for the given record type.
213 template <typename Record>
214 std::vector<std::string> CreateTableString(SqlServerType serverType);
215
216 /// Constructs a string list of SQL queries to create the tables for the given record types.
217 template <typename FirstRecord, typename... MoreRecords>
218 std::vector<std::string> CreateTablesString(SqlServerType serverType);
219
220 /// Creates the table for the given record type.
221 template <typename Record>
222 void CreateTable();
223
224 /// Creates the tables for the given record types.
225 template <typename FirstRecord, typename... MoreRecords>
226 void CreateTables();
227
228 /// @brief Creates a new record in the database.
229 ///
230 /// The record is inserted into the database and the primary key is set on this record.
231 ///
232 /// Every auto-maintained timestamp field (see @ref FieldTimestamp) that holds no value is set to the
233 /// current time of the timestamp clock (see SetTimestampClock()) on this record and inserted with it; a
234 /// timestamp the caller set is inserted as given.
235 ///
236 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
237 /// @tparam Record The record type to insert.
238 /// @param record The record to insert. The primary key field is updated in-place after the insert.
239 /// @return The primary key of the newly created record.
240 template <DataMapperOptions QueryOptions = {}, typename Record>
241 RecordPrimaryKeyType<Record> Create(Record& record);
242
243 /// @brief Creates a new record in the database.
244 ///
245 /// @note This is a variation of the Create() method and does not update the record's primary key.
246 ///
247 /// Auto-maintained timestamp fields (see @ref FieldTimestamp) that hold no value are inserted with the
248 /// current time of the timestamp clock, as Create() does, but the record itself is left untouched.
249 ///
250 /// @tparam Record The record type to insert.
251 /// @param record The record to insert. Unlike Create(), the primary key field is NOT updated in-place.
252 /// @return The primary key of the newly created record.
253 template <typename Record>
254 RecordPrimaryKeyType<Record> CreateExplicit(Record const& record);
255
256 /// @brief Batch-inserts a span of records with a single prepared statement.
257 ///
258 /// The INSERT is prepared once and the whole batch is submitted via
259 /// SqlStatement::ExecuteBatch(rows, accessors...), which uses native zero-copy row-wise array
260 /// binding when every inserted column is row-bindable (primitives, date/time/datetime, numeric, or
261 /// std::optional of a fixed non-numeric type) and the driver supports parameter arrays, otherwise a
262 /// prepare-once + per-row execute. This is dramatically faster than calling CreateExplicit() in a
263 /// loop (which re-prepares per row).
264 ///
265 /// @note Like CreateExplicit(), this does not write back primary keys, relations, or modified-state
266 /// onto the records; callers should treat the inserted records as write-only inputs. Auto-increment
267 /// primary keys are not retrieved.
268 ///
269 /// Auto-maintained timestamp fields (see @ref FieldTimestamp) that hold no value are inserted with the
270 /// current time of the timestamp clock, read once for the whole batch. For such a record type the batch
271 /// is copied to carry the timestamps, so the record type must then be copy-constructible.
272 ///
273 /// Accepts any contiguous, sized range of records (e.g. std::vector, std::array, std::span, or a C
274 /// array), so `dm.CreateAll(records)` works without an explicit std::span wrapper. Non-contiguous
275 /// ranges are rejected at compile time via static_assert (no implicit copy is made).
276 ///
277 /// @tparam Records A contiguous range whose element type is the record type to insert.
278 /// @param records The records to insert. An empty range is a no-op.
279 template <std::ranges::range Records>
280 void CreateAll(Records const& records);
281
282 /// @brief Creates a copy of an existing record in the database.
283 ///
284 /// This method is useful for duplicating a database record while assigning a new primary key.
285 /// All fields except primary key(s) are copied from the original record.
286 /// The primary key is automatically generated (auto-incremented or auto-assigned).
287 /// Auto-maintained timestamp fields (see @ref FieldTimestamp) are not copied either: the copy is a new
288 /// row, so they are inserted with the current time of the timestamp clock.
289 ///
290 /// @param originalRecord The record to copy.
291 /// @return The primary key of the newly created record.
292 template <DataMapperOptions QueryOptions = {}, typename Record>
293 [[nodiscard]] RecordPrimaryKeyType<Record> CreateCopyOf(Record const& originalRecord);
294
295 /// @brief Queries a single record (based on primary key) from the database.
296 ///
297 /// The primary key(s) are used to identify the record to load.
298 /// If the record is not found, std::nullopt is returned.
299 ///
300 /// @tparam Record The record type to query and materialize.
301 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior,
302 /// such as whether related records should be auto-loaded. For example,
303 /// set the relation loading option to false to disable auto-loading of
304 /// relations when reading a single record.
305 /// @tparam PrimaryKeyTypes The type(s) of the primary key value(s) used to look up the record.
306 /// @param primaryKeys The primary key value(s) identifying the record to load.
307 /// @return An initialized Record if found; otherwise std::nullopt.
308 ///
309 /// @code
310 /// // Example: disable auto-loading of relations when querying a single record
311 /// auto result = dataMapper
312 /// .QuerySingle<MyRecord, DataMapperOptions{ .loadRelations = false }>(primaryKeyValue);
313 /// if (result)
314 /// {
315 /// // use *result; relations have not been auto-loaded
316 /// }
317 /// @endcode
318 template <typename Record, DataMapperOptions QueryOptions = {}, typename... PrimaryKeyTypes>
319 std::optional<Record> QuerySingle(PrimaryKeyTypes&&... primaryKeys);
320
321 /// Queries multiple records from the database, based on the given query.
322 ///
323 /// @tparam Record The record type to query and materialize.
324 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
325 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
326 /// @param selectQuery The composed SQL select query to execute.
327 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
328 /// @return A vector of records populated from the query results.
329 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
330 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
331
332 /// Queries multiple records from the database, based on the given query.
333 ///
334 /// @param sqlQueryString The SQL query string to execute.
335 /// @param inputParameters The input parameters for the query to be bound before executing.
336 /// @return A vector of records of the given type that were found via the query.
337 ///
338 /// example:
339 /// @code
340 /// struct Person
341 /// {
342 /// int id;
343 /// std::string name;
344 /// std::string email;
345 /// std::string phone;
346 /// std::string address;
347 /// std::string city;
348 /// std::string country;
349 /// };
350 ///
351 /// void example(DataMapper& dm)
352 /// {
353 /// auto const sqlQueryString = R"(SELECT * FROM "Person" WHERE "city" = ? AND "country" = ?)";
354 /// auto const records = dm.Query<Person>(sqlQueryString, "Berlin", "Germany");
355 /// for (auto const& record: records)
356 /// {
357 /// std::println("Person: {}", DataMapper::Inspect(record));
358 /// }
359 /// }
360 /// @endcode
361 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
362 std::vector<Record> Query(std::string_view sqlQueryString, InputParameters&&... inputParameters);
363
364 /// Queries records from the database, based on the given query and can be used to retrieve only part of the record
365 /// by specifying the ElementMask.
366 ///
367 /// @tparam ElementMask A SqlElements<Idx...> specialization specifying the zero-based field indices to populate.
368 /// @tparam Record The record type to query and materialize.
369 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
370 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
371 /// @param selectQuery The composed SQL select query to execute. Only the columns listed in the SELECT clause
372 /// are bound; the remaining fields of Record are left at their default values.
373 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
374 /// @return A vector of partially populated records; only fields at the specified indices are filled in.
375 ///
376 /// @code
377 ///
378 /// struct Person
379 /// {
380 /// Field<int> id;
381 /// Field<std::string> name; // index 1
382 /// Field<std::string> email;
383 /// Field<std::string> phone;
384 /// Field<std::string> address;
385 /// Field<std::string> city; // index 5
386 /// Field<std::string> country;
387 /// };
388 ///
389 /// void example(DataMapper& dm)
390 /// {
391 /// auto const query = dm.FromTable(RecordTableName<Person>)
392 /// .Select()
393 /// .Fields({ "name"sv, "city"sv })
394 /// .All();
395 /// auto const infos = dm.Query<SqlElements<1, 5>, Person>(query);
396 /// for (auto const& info : infos)
397 /// {
398 /// // only info.name and info.city are populated
399 /// }
400 /// }
401 /// @endcode
402 template <typename ElementMask, typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
403 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
404
405 /// Queries records of different types from the database, based on the given query.
406 /// User can constructed query that selects columns from the multiple tables
407 /// this function is used to get result of the query
408 ///
409 /// @tparam First The first record type to materialize from each result row.
410 /// @tparam Second The second record type to materialize from each result row.
411 /// @tparam Rest Zero or more additional record types to materialize from each result row.
412 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
413 /// @param selectQuery The composed SQL select query whose column list covers all fields of First, Second, and Rest.
414 /// @return A vector of tuples, each containing one instance of every requested record type per result row.
415 ///
416 /// @code
417 ///
418 /// struct JointA{};
419 /// struct JointB{};
420 /// struct JointC{};
421 ///
422 /// // the following query will construct statement to fetch all elements of JointA and JointC types
423 /// auto dm = DataMapper {};
424 /// auto const query = dm.FromTable(RecordTableName<JoinTestA>)
425 /// .Select()
426 /// .Fields<JointA, JointC>()
427 /// .InnerJoin<&JointB::a_id, &JointA::id>()
428 /// .InnerJoin<&JointC::id, &JointB::c_id>()
429 /// .All();
430 /// auto const records = dm.Query<JointA, JointC>(query);
431 /// for(const auto [elementA, elementC] : records)
432 /// {
433 /// // do something with elementA and elementC
434 /// }
435 /// @endcode
436 template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions = {}>
437 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
438 std::vector<std::tuple<First, Second, Rest...>> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery);
439
440 /// Queries records of given Record type.
441 ///
442 /// The query builder can be used to further refine the query.
443 /// The query builder will execute the query when a method like All(), First(n), etc. is called.
444 ///
445 /// @tparam Record The record type to query and materialize.
446 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
447 /// @return A query builder for the given Record type.
448 ///
449 /// @code
450 /// auto const records = dm.Query<Person>()
451 /// .Where(FieldNameOf<&Person::is_active>, "=", true)
452 /// .All();
453 /// @endcode
454 template <typename Record, DataMapperOptions QueryOptions = {}>
456 {
457 return SqlAllFieldsQueryBuilder<Record, QueryOptions>(*this, BuildFullyQualifiedFieldList<Record>());
458 }
459
460 /// Asynchronous counterpart of @c Query — returns an async query builder for @p Record.
461 ///
462 /// The builder offers the exact same fluent DSL (`Where`, `OrderBy`, `GroupBy`, joins, …) as the
463 /// synchronous one; its finisher methods (`All()`, `First()`, `First(n)`, `Range()`, `Count()`,
464 /// `Exist()`, `Delete()`) return an @c Async::Task instead of the plain result, to be @c co_await -ed.
465 /// The connection must have been put into async mode via @c SqlConnection::EnableAsync first.
466 ///
467 /// @note The returned builder is a temporary; keep the whole chain in the @c co_await full-expression
468 /// (e.g. `co_await dm.QueryAsync<Person>().Where(...).All();`) so it outlives the awaited task.
469 ///
470 /// @tparam Record The record type to query and materialize.
471 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
472 /// @return An asynchronous query builder for the given Record type.
473 ///
474 /// @code
475 /// auto const records = co_await dm.QueryAsync<Person>()
476 /// .Where(FieldNameOf<&Person::is_active>, "=", true)
477 /// .All();
478 /// @endcode
479 template <typename Record, DataMapperOptions QueryOptions = {}>
485
486 /// Returns a SqlQueryBuilder using the default query formatter.
487 ///
488 /// This can be used to build custom queries separately from the DataMapper
489 /// and execute them via the DataMapper's typed Query() overloads that accept a SqlSelectQueryBuilder.
490 ///
491 /// @return A SqlQueryBuilder bound to the connection's query formatter.
493 {
494 return SqlQueryBuilder(_connection.QueryFormatter());
495 }
496
497 /// Updates the record in the database.
498 ///
499 /// Only fields that have been modified since the record was last loaded or saved are written.
500 /// Fields that were not changed are excluded from the UPDATE statement. If no field is
501 /// modified, the call is a no-op and no statement is executed.
502 ///
503 /// When at least one field other than an UpdatedAt timestamp (see @ref FieldTimestamp) is written, every
504 /// UpdatedAt field is set to the current time of the timestamp clock on this record and written with it,
505 /// unless the caller assigned it a value in this change. CreatedAt fields are never touched.
506 ///
507 /// The record type must have a primary key: the WHERE clause is built exclusively from the
508 /// primary-key fields, so a record without one would produce an UPDATE with no WHERE clause
509 /// and rewrite every row of the table. Use the query builder's Update() with an explicit
510 /// WHERE clause when you need to update a table that has no primary key. UpdateAll() carries
511 /// the same requirement.
512 ///
513 /// @tparam Record The record type to update. Must have a primary key.
514 /// @param record The record to update. Only its modified fields are written to the database.
515 template <typename Record>
516 requires HasPrimaryKey<Record>
517 void Update(Record& record);
518
519 /// @brief Batch-updates a span of records with a single prepared statement.
520 ///
521 /// One UPDATE is prepared that writes **all** storable non-primary-key columns of the record,
522 /// matched on the primary key(s) (`UPDATE … SET <all non-PK columns> WHERE <pk> = ?`), and the whole
523 /// batch is submitted via SqlStatement::ExecuteBatch(rows, accessors...) — natively row-wise when
524 /// possible, otherwise prepare-once + per-row execute.
525 ///
526 /// @note Unlike Update(), which writes only the modified fields of a single record, this writes a
527 /// uniform set of columns for every row, because a single prepared statement must bind the same
528 /// columns for the whole batch. Per-row modified-state is therefore not consulted, and is not reset.
529 ///
530 /// Every UpdatedAt timestamp field (see @ref FieldTimestamp) is written with the current time of the
531 /// timestamp clock, read once for the whole batch; the records themselves are left untouched. For such a
532 /// record type the batch is copied to carry the timestamps, so the record type must then be
533 /// copy-constructible.
534 ///
535 /// Accepts any contiguous, sized range of records (see CreateAll), so `dm.UpdateAll(records)` works
536 /// without an explicit std::span wrapper. Non-contiguous ranges are rejected at compile time.
537 ///
538 /// @tparam Records A contiguous range whose element type is the record type to update (with a primary key).
539 /// @param records The records to update. An empty range is a no-op.
540 template <std::ranges::range Records>
541 void UpdateAll(Records const& records);
542
543 /// Deletes the record from the database.
544 ///
545 /// The record is identified by its primary key(s). The row is removed from the backing table.
546 ///
547 /// @tparam Record The record type to delete.
548 /// @param record The record to delete. Its primary key field(s) identify the row to remove.
549 /// @return The number of rows deleted (typically 1 if the record was found, 0 otherwise).
550 template <typename Record>
551 std::size_t Delete(Record const& record);
552
553 /// Constructs an SQL query builder for the given table name.
554 SqlQueryBuilder FromTable(std::string_view tableName)
555 {
556 return _connection.Query(tableName);
557 }
558
559 /// Checks if the record has any modified fields.
560 ///
561 /// @tparam Record The record type to inspect.
562 /// @param record The record to check.
563 /// @return True if at least one field has been modified since the record was last loaded or saved.
564 template <typename Record>
565 bool IsModified(Record const& record) const noexcept;
566
567 /// Enum to set the modified state of a record.
568 enum class ModifiedState : uint8_t
569 {
570 Modified,
571 NotModified
572 };
573
574 /// Sets the modified state of the record after receiving from the database.
575 /// This marks all fields as not modified.
576 ///
577 /// @tparam state The target modified state for all fields (Modified or NotModified).
578 /// @tparam Record The record type whose fields are to be updated.
579 /// @param record The record whose field modification flags are set to @p state.
580 template <ModifiedState state, typename Record>
581 void SetModifiedState(Record& record) noexcept;
582
583 /// Loads all direct relations to this record.
584 ///
585 /// @tparam Record The record type whose relation fields are to be populated.
586 /// @param record The record whose BelongsTo, HasMany, HasOneThrough, and HasManyThrough fields are loaded.
587 template <typename Record>
588 void LoadRelations(Record& record);
589
590 /// Configures the auto loading of relations for the given record.
591 ///
592 /// This means, that no explicit loading of relations is required.
593 /// The relations are automatically loaded when accessed.
594 ///
595 /// @tparam Record The record type to configure auto-loading for.
596 /// @param record The record whose relation fields are set up to load lazily on first access.
597 template <typename Record>
598 void ConfigureRelationAutoLoading(Record& record);
599
600 /// Helper function that allow to execute query directly via data mapper
601 /// and get scalar result without need to create SqlStatement manually
602 ///
603 /// @tparam T The scalar type of the expected result value.
604 /// @param sqlQueryString The SQL query string to execute.
605 /// @return The first column of the first result row cast to T, or std::nullopt if the query returns no rows.
606 template <typename T>
607 [[nodiscard]] std::optional<T> Execute(std::string_view sqlQueryString);
608
609 // --------------------------------------------------------------------------------------------
610 // Asynchronous (C++23 coroutine) API.
611 //
612 // Each method offloads its synchronous counterpart to the connection's async backend — a
613 // worker thread, serialized per connection — and resumes the awaiting coroutine on the app's
614 // resume scheduler. Call SqlConnection::EnableAsync(...) on the underlying connection (or use a
615 // pool that stamps it) before invoking any of these. Definitions live in
616 // Async/DataMapperAsync.hpp (included at the end of this header).
617 //
618 // Methods taking a Record& / Record const& capture the record BY REFERENCE, and dereference it
619 // on a worker thread when the returned Task is awaited. The caller must keep the record alive —
620 // and must not mutate or move it — for the entire duration of the co_await (i.e. until the
621 // awaiting coroutine resumes), not merely until the call returns. Destroying, moving, or mutating
622 // it before the co_await resumes is a use-after-free / data race. The idiomatic, safe form keeps
623 // the whole expression in the co_await: `co_await dm.UpdateAsync(record);`.
624
625 /// Asynchronously inserts @p record, updating its primary key in place. @see Create.
626 template <DataMapperOptions QueryOptions = {}, typename Record>
627 [[nodiscard]] Async::Task<RecordPrimaryKeyType<Record>> CreateAsync(Record& record);
628
629 /// Asynchronously queries a single record by its primary key(s). @see QuerySingle.
630 ///
631 /// This is the asynchronous shorthand for a primary-key lookup; for anything else use the fluent
632 /// builder returned by QueryAsync<Record>() (whose finishers also return an Async::Task). Note there
633 /// is deliberately no QueryAsync(string)/QueryAsync(ComposedQuery) — that is what the builder is for.
634 template <typename Record, DataMapperOptions QueryOptions = {}, typename... PrimaryKeyTypes>
635 [[nodiscard]] Async::Task<std::optional<Record>> QuerySingleAsync(PrimaryKeyTypes... primaryKeys);
636
637 /// Asynchronously updates @p record's modified fields. @see Update.
638 template <typename Record>
639 [[nodiscard]] Async::Task<void> UpdateAsync(Record& record);
640
641 /// Asynchronously deletes @p record. @see Delete.
642 template <typename Record>
643 [[nodiscard]] Async::Task<std::size_t> DeleteAsync(Record const& record);
644
645 /// Asynchronously loads @p record's relations. @see LoadRelations.
646 template <typename Record>
647 [[nodiscard]] Async::Task<void> LoadRelationsAsync(Record& record);
648
649 private:
650 // The fluent query builders drive the batched relation preloading (`With<>()`), which is an
651 // internal entry point rather than part of the mapper's public surface.
652 template <typename BuilderRecord, typename Derived, DataMapperOptions BuilderQueryOptions>
653 friend class SqlCoreDataMapperQueryBuilder;
654
655 friend void detail::AdoptRelationLoadSource(DataMapper& dataMapper,
656 std::shared_ptr<detail::RelationLoadSource> source) noexcept;
657
658 /// The source this mapper's relation auto-loaders borrow from (see @ref detail::RelationLoadSource).
659 ///
660 /// Chosen on first use unless adopted from a pool: the process-wide pool when this mapper is
661 /// connected with the current default connection string, otherwise a source that reconnects with
662 /// this mapper's own connection string, so that relations load from the database the record was
663 /// read from.
664 ///
665 /// @return The source; never null.
666 [[nodiscard]] LIGHTWEIGHT_API std::shared_ptr<detail::RelationLoadSource> const& RelationLoadSourceForLoaders();
667
668 /// Runs @p query and hands each row to @p each as an auto-loading @p Record, reusing one instance.
669 ///
670 /// Backs the streaming @c Each() of the relation loaders. Runs on this mapper's own statement,
671 /// which is safe because the loaders only ever call it on a mapper borrowed for that one load.
672 ///
673 /// @param query The query to run.
674 /// @param each Called once per row.
675 /// @param inputParameters Bound to the query's parameters.
676 template <typename Record, typename QueryText, typename Callable, typename... InputParameters>
677 void StreamRecords(QueryText const& query, Callable const& each, InputParameters const&... inputParameters);
678
679 /// Runs one relation load on a mapper borrowed from @p source, reporting every failure as a
680 /// @ref RelationError rather than an exception: an unusable source as whatever @c Borrow() reports,
681 /// a failing query as @ref RelationError::QueryFailed (its diagnostic goes to @c SqlLogger).
682 ///
683 /// @param source Where the load borrows its mapper from.
684 /// @param load Runs the query on the borrowed mapper and returns its result.
685 /// @return What @p load returned, or why the load did not happen.
686 template <typename Load>
687 static auto RunRelationLoad(detail::RelationLoadSource& source, Load const& load)
688 -> RelationResult<std::invoke_result_t<Load const&, DataMapper&>>;
689
690 /// @ref RunRelationLoad for the streaming @c Each() loaders. An exception thrown by the caller's
691 /// @p each callback is not a failed load: it propagates unchanged instead of being reported.
692 ///
693 /// @param source Where the load borrows its mapper from.
694 /// @param each The caller's per-record callback.
695 /// @param stream Runs the query on the borrowed mapper, calling the callback it is given per row.
696 /// @return Nothing, or why the load did not happen.
697 template <typename Each, typename Stream>
698 static RelationResult<void> RunRelationStream(detail::RelationLoadSource& source,
699 Each const& each,
700 Stream const& stream);
701
702 /// @return @p record, or @ref RelationError::NotFound for a null one.
703 template <typename Record>
704 static RelationResult<std::shared_ptr<Record>> NotFoundIfNull(std::shared_ptr<Record> record)
705 {
706 if (!record)
707 return std::unexpected { RelationError::NotFound };
708 return record;
709 }
710
711 /// Builds the comma-separated, fully-qualified (`"Table"."Column"`) field list for @p Record.
712 ///
713 /// Shared by @c Query and @c QueryAsync so the SELECT projection is produced in exactly one place.
714 ///
715 /// @tparam Record The record type whose members are enumerated.
716 /// @return The field list usable as the projection of a SELECT statement.
717 template <typename Record>
718 [[nodiscard]] static std::string BuildFullyQualifiedFieldList()
719 {
720 std::string fields;
721 EnumerateRecordMembers<Record>([&fields]<size_t I, typename FieldType>() {
722 // Relations (HasMany, HasManyThrough, HasOneThrough, ...) have no column of their own.
723 if constexpr (RecordColumnMember<FieldType>)
724 {
725 if (!fields.empty())
726 fields += ", ";
727 fields += '"';
728 fields += RecordTableName<Record>;
729 fields += "\".\"";
730 fields += FieldNameAt<I, Record>;
731 fields += '"';
732 }
733 });
734 return fields;
735 }
736
737 /// @brief Queries a single record from the database based on the given query.
738 ///
739 /// @param selectQuery The SQL select query to execute.
740 /// @param args The input parameters for the query.
741 ///
742 /// @return The record if found, otherwise std::nullopt.
743 template <typename Record, typename... Args>
744 std::optional<Record> QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args);
745
746 template <typename Record, typename ValueType>
747 void SetId(Record& record, ValueType&& id);
748
749 template <typename Record, size_t InitialOffset = 1>
750 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
751
752 template <typename ElementMask, typename Record, size_t InitialOffset = 1>
753 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
754
755 template <typename FieldType>
756 std::optional<typename FieldType::ReferencedRecord> LoadBelongsTo(FieldType::ValueType value);
757
758 /// Queries the record referenced by a composite foreign key, without touching the relation itself.
759 ///
760 /// Shared by the eager path (`LoadCompositeForeignKey`) and the lazy loader installed by
761 /// `ConfigureRelationAutoLoading`, so both resolve a missing target row and wrap a found one the
762 /// same way instead of maintaining two copies of that logic.
763 ///
764 /// Takes the already-permuted key values rather than the owning record itself: the lazy loader
765 /// must evaluate `FieldType::OrderedValuesOf()` while the record is known to be live (at
766 /// `ConfigureRelationAutoLoading` time) and capture the resulting values by value, not a pointer to
767 /// the record - a `std::optional<Record>` returned by value from a query method is not guaranteed to
768 /// stay at the same address (NRVO is not mandated by the standard, and does not reliably apply to
769 /// every such function in practice), so a captured pointer can dangle by the time the loader runs.
770 ///
771 /// @param keys The foreign key values, in the referenced record's member order.
772 /// @return The referenced record, or `nullptr` if no matching row exists.
773 template <typename FieldType>
774 std::shared_ptr<typename FieldType::ReferencedRecord> LoadCompositeForeignKeyRecord(
775 FieldType::OrderedValueType const& keys);
776
777 /// Eagerly loads the record referenced by a composite foreign key.
778 ///
779 /// @param record The record holding the foreign key.
780 /// @param field The relation to fill.
781 template <typename Record, typename FieldType>
782 void LoadCompositeForeignKey(Record const& record, FieldType& field);
783
784 template <typename Record, typename OtherRecord, auto InverseSelector>
785 void LoadHasMany(Record& record, HasMany<OtherRecord, InverseSelector>& field);
786
787 /// @brief Eagerly resolves one relation for a whole batch of records, in a bounded number of queries.
788 ///
789 /// This is the engine behind `Query<Record>().With<&Record::relation>()`. Where the lazy loaders
790 /// installed by `ConfigureRelationAutoLoading()` issue one query per record touched - the N+1 - this
791 /// issues one query per chunk of the batch (see `SqlQueryFormatter::MaxInPredicateValues()`) and
792 /// distributes the resulting rows into the records in memory.
793 ///
794 /// The batch is addressed by pointer rather than as a contiguous range: past the first level of
795 /// a relation path the targets are not contiguous - every owner holds its own copy of a
796 /// `BelongsTo` target, and a `HasMany` list holds `shared_ptr`s - so only their addresses can be
797 /// gathered. `SqlCoreDataMapperQueryBuilder::RunRelationPreloaders()` adapts a freshly
798 /// materialized result set onto this.
799 ///
800 /// @tparam Record The record type owning the relation.
801 /// @tparam FieldIndex Member index, within @p Record, of the relation to load.
802 /// @param records The batch to resolve the relation for.
803 template <typename Record, size_t FieldIndex>
804 void PreloadRelation(std::span<Record* const> records);
805
806 /// Batch counterpart of `LoadBelongsTo`: one query per chunk of distinct foreign keys.
807 /// @see PreloadRelation
808 template <typename Record, size_t FieldIndex>
809 void PreloadBelongsTo(std::span<Record* const> records);
810
811 /// Batch counterpart of `LoadHasMany`: one query per chunk of owner primary keys, then the rows
812 /// are grouped by the inverse foreign key and handed to each owner.
813 /// @see PreloadRelation
814 template <typename Record, size_t FieldIndex>
815 void PreloadHasMany(std::span<Record* const> records);
816
817 /// @brief Eagerly resolves a *path* of relations, one bounded set of queries per level.
818 ///
819 /// Backs `Query<Record>().With<&Track::album, &Album::artist>()`: the first relation is resolved
820 /// for the whole batch, the loaded targets are then gathered and the next relation resolved for
821 /// all of them at once, and so on. Every level costs a constant number of queries, so a two-level
822 /// path over any number of records is three queries in total.
823 ///
824 /// @tparam Record The record type owning the first relation of the path.
825 /// @tparam FieldIndex Member index, within @p Record, of the first relation.
826 /// @tparam RestOfPath The remaining relations, each a member of the preceding target type.
827 /// @param records The batch to resolve the path for.
828 template <typename Record, size_t FieldIndex, auto... RestOfPath>
829 void PreloadRelationPath(std::span<Record* const> records);
830
831 /// @brief Eagerly resolves *every* supported relation reachable within @p Depth levels.
832 ///
833 /// Backs `DataMapperOptions { .eagerLoadDepth = N }`. Each level costs a bounded number of
834 /// queries per relation, never one per record. `HasOneThrough`, `HasManyThrough` and
835 /// `CompositeForeignKey` are skipped and keep loading on demand.
836 ///
837 /// @tparam Record The record type whose relations are resolved.
838 /// @tparam Depth Remaining levels to descend; `0` stops the recursion, which is what keeps a
839 /// cyclic relation graph (a self-referencing record, or A -> B -> A) from
840 /// instantiating forever.
841 /// @param records The batch to resolve the relations for.
842 template <typename Record, size_t Depth>
843 void PreloadAllRelations(std::span<Record* const> records);
844
845 /// Appends the addresses of the records loaded into relation @p FieldIndex of every record in
846 /// @p records, so the next level of a path can be resolved for all of them at once.
847 ///
848 /// @tparam Record The record type owning the relation.
849 /// @tparam FieldIndex Member index, within @p Record, of the relation to walk into.
850 /// @param records The batch to collect from.
851 /// @return The loaded targets; records whose relation is unloaded or NULL contribute nothing.
852 template <typename Record, size_t FieldIndex>
853 [[nodiscard]] static auto CollectRelationTargets(std::span<Record* const> records);
854
855 template <typename ReferencedRecord, typename ThroughSpec, typename Record, auto OwnerSelector, auto ThroughSelector>
856 void LoadHasOneThrough(Record& record,
857 HasOneThrough<ReferencedRecord, ThroughSpec, OwnerSelector, ThroughSelector>& field);
858
859 template <typename ReferencedRecord, typename ThroughSpec, typename Record, auto OwnerSelector, auto ReferencedSelector>
860 void LoadHasManyThrough(Record& record,
861 HasManyThrough<ReferencedRecord, ThroughSpec, OwnerSelector, ReferencedSelector>& field);
862
863 template <typename Record, typename OtherRecord, auto InverseSelector, typename Callable>
864 void CallOnHasMany(Record& record, Callable const& callback);
865
866 template <typename OwnerRecord, typename OtherRecord, auto InverseSelector>
867 SqlSelectQueryBuilder BuildHasManySelectQuery();
868
869 /// @brief Builds a `SELECT <all storage columns> FROM <table of @p Record>`, without any predicate.
870 ///
871 /// The starting point for the batched relation queries, which add their own `WHERE ... IN (...)`.
872 ///
873 /// @tparam Record The record type whose table and columns are selected.
874 /// @return The select query builder, ready for further clauses.
875 template <typename Record>
876 SqlSelectQueryBuilder BuildRecordSelectQuery();
877
878 template <typename ReferencedRecord, typename ThroughRecord, typename Record, auto OwnerSelector, auto ThroughSelector>
879 SqlSelectQueryBuilder BuildHasOneThroughSelectQuery();
880
881 template <typename ReferencedRecord,
882 typename ThroughRecord,
883 typename Record,
884 auto OwnerSelector,
885 auto ReferencedSelector>
886 SqlSelectQueryBuilder BuildHasManyThroughSelectQuery();
887
888 template <typename ReferencedRecord,
889 typename ThroughRecord,
890 typename Record,
891 auto OwnerSelector,
892 auto ReferencedSelector,
893 typename Callable>
894 void CallOnHasManyThrough(Record& record, Callable const& callback);
895
896 template <typename ReferencedRecord,
897 typename ThroughRecord,
898 typename Record,
899 auto OwnerSelector,
900 auto ReferencedSelector,
901 typename PKValue,
902 typename Callable>
903 void CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback);
904
905 template <typename ReferencedRecord,
906 typename ThroughRecord,
907 typename Record,
908 auto OwnerSelector,
909 auto ThroughSelector,
910 typename PKValue>
911 std::shared_ptr<ReferencedRecord> LoadHasOneThroughByPK(PKValue const& pkValue);
912
913 enum class PrimaryKeySource : std::uint8_t
914 {
915 Record,
916 Override,
917 };
918
919 /// Which auto-maintained timestamp fields (see @ref FieldTimestamp) a write sets to the current time.
920 enum class TimestampFill : std::uint8_t
921 {
922 /// No field: the record's own values are written (e.g. Create() already stamped the record).
923 None,
924 /// Every timestamp field that holds no value (inserts).
925 Unset,
926 /// Every timestamp field, whatever it holds (CreateCopyOf(): the copy is a new row).
927 All,
928 /// Every UpdatedAt field, whatever it holds (UpdateAll()).
929 UpdatedAt,
930 };
931
932 /// Whether a write that fills timestamps as @p Fill says writes the current time into @p field.
933 template <TimestampFill Fill, typename FieldType>
934 [[nodiscard]] static constexpr bool FillsTimestamp(FieldType const& field) noexcept;
935
936 /// Sets the timestamp fields of @p record that @p Fill selects to @p now.
937 template <TimestampFill Fill, typename Record>
938 static void StampTimestamps(Record& record, SqlDateTime const& now);
939
940 /// Sets the UpdatedAt fields of @p record to the current time, for an update, if the update writes anything else.
941 template <typename Record>
942 void StampTimestampsForUpdate(Record& record) const;
943
944 /// Returns a copy of @p records whose timestamp fields selected by @p Fill hold the current time.
945 template <TimestampFill Fill, typename Record>
946 [[nodiscard]] std::vector<Record> CopyWithTimestamps(std::span<Record const> records) const;
947
948 /// Batch-inserts @p records as given (the body of CreateAll()).
949 template <typename Record>
950 void InsertBatch(std::span<Record const> records);
951
952 /// Batch-updates @p records as given (the body of UpdateAll()).
953 template <typename Record>
954 void UpdateBatch(std::span<Record const> records);
955
956 template <typename Record>
957 std::optional<RecordPrimaryKeyType<Record>> GenerateAutoAssignPrimaryKey(Record const& record);
958
959 template <PrimaryKeySource UsePkOverride, TimestampFill Fill, typename Record>
960 RecordPrimaryKeyType<Record> CreateInternal(
961 Record const& record,
962 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
963 pkOverride = std::nullopt);
964
965 SqlConnection _connection;
966 SqlStatement _stmt;
967 std::shared_ptr<detail::RelationLoadSource> _relationLoadSource;
968 TimestampClock _timestampClock;
969};
970
971inline void detail::AdoptRelationLoadSource(DataMapper& dataMapper, std::shared_ptr<RelationLoadSource> source) noexcept
972{
973 dataMapper._relationLoadSource = std::move(source);
974}
975
976// ------------------------------------------------------------------------------------------------
977
978namespace detail
979{
980 /// Whether @p T is a column type whose buffer may have to grow while fetching (e.g. a string long
981 /// enough to be truncated into the initially bound buffer).
982 ///
983 /// @tparam T The *value* type of the column, i.e. `Field<...>::ValueType`, never the `Field<...>`
984 /// wrapper itself.
985 template <typename T>
986 inline constexpr bool IsGrowableColumnType =
987 detail::OneOf<T, std::string, std::wstring, std::u16string, std::u32string, SqlBinary> || IsSqlDynamicString<T>
988 || IsSqlDynamicBinary<T>;
989
990 /// Whether a single output column of value type @p T can safely be bound up front on @p
991 /// sqlServerType.
992 ///
993 /// Takes the column's *value* type, not a `Field<...>` wrapper: the sole caller projects a single
994 /// field and only has `ReferencedFieldTypeOf<Field>` (the ValueType) to hand. This previously
995 /// took the wrapper and gated its whole body on `if constexpr (IsField<FieldType>)`, which a
996 /// ValueType never satisfies - so it silently answered "safe to bind" for every type, including
997 /// the growable ones it exists to exclude.
998 ///
999 /// @tparam T The column's value type.
1000 /// @param sqlServerType The server being queried.
1001 /// @return `true` if the column may be bound with `BindOutputColumn`, `false` if it has to be read
1002 /// per row with `GetColumn` instead.
1003 template <typename T>
1004 constexpr bool CanSafelyBindOutputColumn(SqlServerType sqlServerType) noexcept
1005 {
1006 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
1007 return true;
1008
1009 // Test if we have some columns that might not be sufficient to store the result (e.g. string truncation),
1010 // then don't call BindOutputColumn but SQLFetch to get the result, because
1011 // regrowing previously bound columns is not supported in MS-SQL's ODBC driver, so it seems.
1012 return !IsGrowableColumnType<T>;
1013 }
1014
1015 /// Whether every output column of @p Record can safely be bound up front on @p sqlServerType.
1016 ///
1017 /// The record-wide counterpart of @ref CanSafelyBindOutputColumn: one growable member is enough to
1018 /// force the whole record onto the per-row `GetColumn` path.
1019 ///
1020 /// @tparam Record The record being fetched.
1021 /// @param sqlServerType The server being queried.
1022 /// @return `true` if the columns may be bound with `BindOutputColumns`.
1023 template <DataMapperRecord Record>
1024 constexpr bool CanSafelyBindOutputColumns(SqlServerType sqlServerType) noexcept
1025 {
1026 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
1027 return true;
1028
1029 bool result = true;
1030 EnumerateRecordMembers<Record>([&result]<size_t I, typename Field>() {
1031 if constexpr (IsField<Field>)
1032 if constexpr (IsGrowableColumnType<typename Field::ValueType>)
1033 // Known types that MAY require growing due to truncation.
1034 result = false;
1035 });
1036 return result;
1037 }
1038
1039 template <typename Record>
1040 void BindAllOutputColumnsWithOffset(SqlResultCursor& reader, Record& record, SQLUSMALLINT startOffset)
1041 {
1042 EnumerateRecordMembers(record, [reader = &reader, i = startOffset]<size_t I, typename Field>(Field& field) mutable {
1043 if constexpr (IsField<Field>)
1044 {
1045 reader->BindOutputColumn(i++, &field.MutableValue());
1046 }
1047 else if constexpr (IsBelongsTo<Field>)
1048 {
1049 reader->BindOutputColumn(i++, &field.MutableValue());
1050 }
1051 else if constexpr (SqlOutputColumnBinder<Field>)
1052 {
1053 reader->BindOutputColumn(i++, &field);
1054 }
1055 });
1056 }
1057
1058 template <typename Record>
1059 void BindAllOutputColumns(SqlResultCursor& reader, Record& record)
1060 {
1061 BindAllOutputColumnsWithOffset(reader, record, 1);
1062 }
1063
1064 /// @brief Requested rows per SQLFetchScroll round-trip for the native row-wise fetch fast path. The
1065 /// statement clamps this to a memory budget, so it is an upper bound, not a guarantee.
1066 constexpr std::size_t kDefaultRowArrayFetchDepth = 1024;
1067
1068 /// @brief Mutable-reference output accessor for member @p I that is a Field/BelongsTo: yields the
1069 /// field's mutable value so the row-wise fetch path binds the result column in place. The read-side
1070 /// counterpart of @ref FieldValueAccessor.
1071 template <std::size_t I>
1072 struct MutableFieldValueAccessor
1073 {
1074 template <typename Record>
1075 decltype(auto) operator()(Record& record) const
1076 {
1077 return GetRecordMemberAt<I>(record).MutableValue();
1078 }
1079 };
1080
1081 /// @brief The mutable value type bound for member @p FieldType on the row-wise fetch path (the type
1082 /// the result column materializes into).
1083 template <typename FieldType>
1084 using RowWiseColumnValueType = std::remove_cvref_t<decltype(std::declval<FieldType&>().MutableValue())>;
1085
1086 /// @return Whether @p FieldType maps to a result column on the bound-output path (Field, BelongsTo, or
1087 /// a directly-bindable member) — mirrors the classification in @ref BindAllOutputColumnsWithOffset.
1088 template <typename FieldType>
1089 constexpr bool RowWiseIsColumn()
1090 {
1091 return IsField<FieldType> || IsBelongsTo<FieldType> || SqlOutputColumnBinder<FieldType>;
1092 }
1093
1094 /// @return Whether @p FieldType is acceptable on the row-wise fetch path: either it is not a result
1095 /// column (a relation member, which is not bound) or it is a column whose value type is
1096 /// @ref SqlRowWiseFetchableColumn. Directly-bindable non-Field members are conservatively rejected
1097 /// (their value would need a separate accessor shape) so such records fall back to the per-row path.
1098 template <typename FieldType>
1099 constexpr bool RowWiseColumnAcceptable()
1100 {
1101 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
1102 return SqlRowWiseFetchableColumn<RowWiseColumnValueType<FieldType>>;
1103 else if constexpr (SqlOutputColumnBinder<FieldType>)
1104 return false;
1105 else
1106 return true; // relation / non-column member: not bound, imposes no constraint
1107 }
1108
1109 template <typename Record, std::size_t... Is>
1110 constexpr bool CanRowWiseFetchRecordImpl(std::index_sequence<Is...> /*indices*/)
1111 {
1112 // The row-strided indicator slots are addressed at i * sizeof(Record); they must stay SQLLEN
1113 // aligned, so sizeof(Record) must be a multiple of alignof(SQLLEN) (mirrors the write-side
1114 // indicatorAlignmentSatisfied precondition).
1115 return (sizeof(Record) % alignof(SQLLEN) == 0) && (RowWiseColumnAcceptable<RecordMemberTypeOf<Is, Record>>() && ...)
1116 && (RowWiseIsColumn<RecordMemberTypeOf<Is, Record>>() || ...);
1117 }
1118
1119 /// @brief Whether @p Record can be materialized via the native row-wise array-fetch fast path: every
1120 /// result column is a Field/BelongsTo of a @ref SqlRowWiseFetchableColumn type, there is at least one
1121 /// column, and the record size keeps the row-strided indicators aligned. Records that fail this fall
1122 /// back to the per-row @c SQLFetch path, with identical results.
1123 template <typename Record>
1124 constexpr bool CanRowWiseFetchRecord()
1125 {
1126 return CanRowWiseFetchRecordImpl<Record>(std::make_index_sequence<RecordMemberCount<Record>> {});
1127 }
1128
1129 /// Returns a one-element accessor tuple for member @p I when it is a bound result column, else an empty
1130 /// tuple — flattened via tuple_cat so the accessor pack matches the bound column set and order exactly.
1131 template <std::size_t I, typename Record>
1132 auto MakeOutputColumnAccessor()
1133 {
1134 using FieldType = RecordMemberTypeOf<I, Record>;
1135 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
1136 return std::tuple<MutableFieldValueAccessor<I>> {};
1137 else
1138 return std::tuple<> {};
1139 }
1140
1141 /// @brief Materializes the whole result set into @p records via @ref SqlStatement::FetchAllRowWise,
1142 /// building one mutable value accessor per bound result column (same set and order as
1143 /// @ref BindAllOutputColumnsWithOffset). Precondition: @ref CanRowWiseFetchRecord<Record>().
1144 template <typename Record>
1145 void ReadAllRowWise(SqlResultCursor& reader, std::vector<Record>* records)
1146 {
1147 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
1148 std::apply(
1149 [&](auto const&... accessors) {
1150 reader.FetchAllRowWise(*records, kDefaultRowArrayFetchDepth, accessors...);
1151 },
1152 std::tuple_cat(MakeOutputColumnAccessor<Is, Record>()...));
1153 }(std::make_index_sequence<RecordMemberCount<Record>> {});
1154 }
1155
1156 /// @return Whether @p FieldType is a result column whose value is a char fixed-capacity string (or a
1157 /// @c std::optional of one). Such columns are array-bound narrow (SQL_C_CHAR), which only round-trips
1158 /// byte-exact where @ref SqlConnection::RoundTripsNarrowTextByteExact holds.
1159 template <typename FieldType>
1160 constexpr bool ColumnIsNarrowFixedString()
1161 {
1162 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
1163 {
1164 using V = RowWiseColumnValueType<FieldType>;
1165 if constexpr (SqlIsStdOptional<V>)
1166 return IsSqlFixedString<typename V::value_type>;
1167 else
1168 return IsSqlFixedString<V>;
1169 }
1170 else
1171 return false;
1172 }
1173
1174 template <typename Record, std::size_t... Is>
1175 constexpr bool RecordHasNarrowFixedStringColumnImpl(std::index_sequence<Is...> /*indices*/)
1176 {
1177 return (ColumnIsNarrowFixedString<RecordMemberTypeOf<Is, Record>>() || ...);
1178 }
1179
1180 /// @brief Whether @p Record has any char fixed-capacity-string result column. Such records take the
1181 /// row-wise fetch fast path only on backends that round-trip narrow text byte-exact; elsewhere they
1182 /// fall back to the per-row (wide) path. See @ref SqlConnection::RoundTripsNarrowTextByteExact.
1183 template <typename Record>
1184 constexpr bool RecordHasNarrowFixedStringColumn()
1185 {
1186 return RecordHasNarrowFixedStringColumnImpl<Record>(std::make_index_sequence<RecordMemberCount<Record>> {});
1187 }
1188
1189 /// @brief Whether @p Record may use the row-wise fetch fast path on @p serverType: it is row-wise
1190 /// fetchable, the driver supports row-array fetch, and any narrow fixed-string column round-trips
1191 /// byte-exact there. Single runtime gate composed from connection capabilities + the compile-time
1192 /// record shape, so business logic never branches on the server type directly.
1193 template <typename Record>
1194 bool CanRowWiseFetchOn(SqlServerType serverType)
1195 {
1196 if constexpr (!CanRowWiseFetchRecord<Record>())
1197 return false;
1198 else
1200 && (!RecordHasNarrowFixedStringColumn<Record>()
1202 }
1203
1204 // --- Two-record tuple (JOIN) fast path ----------------------------------------------------------
1205
1206 /// @brief Mutable-reference output accessor for member @p I of the @p TupleIndex-th sub-record of a
1207 /// @c std::tuple result row; yields that field's mutable value so a JOIN result binds in place.
1208 template <std::size_t TupleIndex, std::size_t I>
1209 struct MutableTupleFieldAccessor
1210 {
1211 template <typename TupleType>
1212 decltype(auto) operator()(TupleType& row) const
1213 {
1214 return GetRecordMemberAt<I>(std::get<TupleIndex>(row)).MutableValue();
1215 }
1216 };
1217
1218 template <typename First, typename Second, std::size_t... Fs, std::size_t... Ss>
1219 constexpr bool CanRowWiseFetchTupleImpl(std::index_sequence<Fs...> /*firstIndices*/,
1220 std::index_sequence<Ss...> /*secondIndices*/)
1221 {
1222 return (sizeof(std::tuple<First, Second>) % alignof(SQLLEN) == 0)
1223 && (RowWiseColumnAcceptable<RecordMemberTypeOf<Fs, First>>() && ...)
1224 && (RowWiseColumnAcceptable<RecordMemberTypeOf<Ss, Second>>() && ...)
1225 && ((RowWiseIsColumn<RecordMemberTypeOf<Fs, First>>() || ...)
1226 || (RowWiseIsColumn<RecordMemberTypeOf<Ss, Second>>() || ...));
1227 }
1228
1229 /// @brief Whether a @c std::tuple<First,Second> JOIN row can be materialized via the row-wise fetch
1230 /// fast path: both sub-records' columns are row-bindable and the combined row size keeps the
1231 /// row-strided indicators aligned.
1232 template <typename First, typename Second>
1233 constexpr bool CanRowWiseFetchTuple()
1234 {
1235 return CanRowWiseFetchTupleImpl<First, Second>(std::make_index_sequence<RecordMemberCount<First>> {},
1236 std::make_index_sequence<RecordMemberCount<Second>> {});
1237 }
1238
1239 /// @brief Whether a @c std::tuple<First,Second> JOIN row may use the row-wise fetch fast path on
1240 /// @p serverType (row-wise fetchable + driver supports row-array fetch + any narrow fixed-string
1241 /// column round-trips byte-exact there). The tuple counterpart of @ref CanRowWiseFetchOn.
1242 template <typename First, typename Second>
1243 bool CanRowWiseFetchTupleOn(SqlServerType serverType)
1244 {
1245 if constexpr (!CanRowWiseFetchTuple<First, Second>())
1246 return false;
1247 else
1249 && ((!RecordHasNarrowFixedStringColumn<First>() && !RecordHasNarrowFixedStringColumn<Second>())
1251 }
1252
1253 /// Accessor tuple for member @p I of the @p TupleIndex-th sub-record, or empty for non-columns.
1254 template <std::size_t TupleIndex, std::size_t I, typename SubRecord>
1255 auto MakeTupleColumnAccessor()
1256 {
1257 using FieldType = RecordMemberTypeOf<I, SubRecord>;
1258 if constexpr (IsField<FieldType> || IsBelongsTo<FieldType>)
1259 return std::tuple<MutableTupleFieldAccessor<TupleIndex, I>> {};
1260 else
1261 return std::tuple<> {};
1262 }
1263
1264 /// @brief Materializes a two-record JOIN result set into @p records via row-wise array fetch. The
1265 /// accessor pack is First's columns followed by Second's, matching the column order of
1266 /// @ref BindAllOutputColumnsWithOffset's offset scheme. Precondition: @ref CanRowWiseFetchTuple.
1267 template <typename First, typename Second>
1268 void ReadAllRowWiseTuple(SqlResultCursor& reader, std::vector<std::tuple<First, Second>>* records)
1269 {
1270 [&]<std::size_t... Fs, std::size_t... Ss>(std::index_sequence<Fs...>, std::index_sequence<Ss...>) {
1271 std::apply(
1272 [&](auto const&... accessors) {
1273 reader.FetchAllRowWise(*records, kDefaultRowArrayFetchDepth, accessors...);
1274 },
1275 std::tuple_cat(MakeTupleColumnAccessor<0, Fs, First>()..., MakeTupleColumnAccessor<1, Ss, Second>()...));
1276 }(std::make_index_sequence<RecordMemberCount<First>> {}, std::make_index_sequence<RecordMemberCount<Second>> {});
1277 }
1278
1279 // when we iterate over all columns using element mask
1280 // indexes of the mask corresponds to the indexe of the field
1281 // inside the structure, not inside the SQL result set
1282 template <typename ElementMask, typename Record>
1283 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
1284 {
1285 EnumerateRecordMembers<ElementMask>(
1286 record, [reader = &reader, &indexFromQuery]<size_t I, typename Field>(Field& field) mutable {
1287 // Only members that map onto a column consume a result set index — relations
1288 // (HasMany, HasManyThrough, HasOneThrough, ...) are not part of the projection.
1289 //
1290 // The projection side (RecordColumnMember, see SqlSelectQueryBuilder::Fields and
1291 // DataMapper::BuildFullyQualifiedFieldList) and this read side must classify every
1292 // member identically; a member that only one of them counts silently shifts the index
1293 // of every column following it. Both predicates ultimately ask whether SqlDataBinder<T>
1294 // is usable as a column, so pin them together here rather than letting them drift.
1295 static_assert(RecordColumnMember<Field> == (IsField<Field> || SqlGetColumnNativeType<Field>),
1296 "Record member is projected but not readable (or readable but not projected). "
1297 "A SqlDataBinder<T> used as a record member must provide both OutputColumn() "
1298 "and GetColumn().");
1299 if constexpr (IsField<Field>)
1300 {
1301 ++indexFromQuery;
1302 if constexpr (Field::IsOptional)
1303 field.MutableValue() =
1304 reader->GetNullableColumn<typename Field::ValueType::value_type>(indexFromQuery);
1305 else
1306 field.MutableValue() = reader->GetColumn<typename Field::ValueType>(indexFromQuery);
1307 }
1308 else if constexpr (SqlGetColumnNativeType<Field>)
1309 {
1310 ++indexFromQuery;
1311 if constexpr (IsOptionalBelongsTo<Field>)
1312 field = reader->GetNullableColumn<typename Field::BaseType>(indexFromQuery);
1313 else
1314 field = reader->GetColumn<Field>(indexFromQuery);
1315 }
1316 });
1317 }
1318
1319 template <typename Record>
1320 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
1321 {
1322 return GetAllColumns<std::make_integer_sequence<size_t, RecordMemberCount<Record>>, Record>(
1323 reader, record, indexFromQuery);
1324 }
1325
1326 template <typename FirstRecord, typename SecondRecord>
1327 // TODO we need to remove this at some points and provide generic bindings for tuples
1328 void GetAllColumns(SqlResultCursor& reader, std::tuple<FirstRecord, SecondRecord>& record)
1329 {
1330 auto& [firstRecord, secondRecord] = record;
1331
1332 // Both sub-records are read through the single-record overload, so relation members are skipped
1333 // (rather than indexed by member position) on both sides. The second sub-record starts after the
1334 // *columns* the first one projects, which is RecordColumnCount, not RecordMemberCount.
1335 GetAllColumns(reader, firstRecord, 0);
1336 GetAllColumns(reader, secondRecord, static_cast<SQLUSMALLINT>(RecordColumnCount<FirstRecord>));
1337 }
1338
1339 template <typename Record>
1340 bool ReadSingleResult(SqlServerType sqlServerType, SqlResultCursor& reader, Record& record)
1341 {
1342 auto const outputColumnsBound = CanSafelyBindOutputColumns<Record>(sqlServerType);
1343
1344 if (outputColumnsBound)
1345 BindAllOutputColumns(reader, record);
1346
1347 if (!reader.FetchRow())
1348 return false;
1349
1350 if (!outputColumnsBound)
1351 GetAllColumns(reader, record);
1352
1353 return true;
1354 }
1355} // namespace detail
1356
1357template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1358template <typename Finisher>
1359auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RunFinisher(Finisher finisher)
1360{
1361 if constexpr (Derived::QueryExecution == SqlQueryExecutionMode::Asynchronous)
1362 return Async::RunAsync(_dm.Connection().AsyncBackend(), std::move(finisher));
1363 else
1364 return finisher();
1365}
1366
1367template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1369 DataMapper& dm, std::string fields) noexcept:
1370 _dm { dm },
1371 _formatter { dm.Connection().QueryFormatter() },
1372 _fields { std::move(fields) }
1373{
1374 this->_query.searchCondition.inputBindings = &_boundInputs;
1375}
1376
1377template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1379 SqlCoreDataMapperQueryBuilder const& other):
1380 SqlBasicSelectQueryBuilder<Derived> { other },
1381 _dm { other._dm },
1382 _formatter { other._formatter },
1383 _fields { other._fields },
1384 _boundInputs { other._boundInputs },
1385 _relationPreloaders { other._relationPreloaders }
1386{
1387 this->_query.searchCondition.inputBindings = &_boundInputs;
1388}
1389
1390template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1392 SqlCoreDataMapperQueryBuilder&& other) noexcept:
1393 // Moves only the base subobject; the members below are still intact in `other`.
1394 SqlBasicSelectQueryBuilder<Derived> { std::move(static_cast<SqlBasicSelectQueryBuilder<Derived>&>(other)) },
1395 _dm { other._dm },
1396 _formatter { other._formatter },
1397 _fields { std::move(other._fields) },
1398 _boundInputs { std::move(other._boundInputs) },
1399 _relationPreloaders { std::move(other._relationPreloaders) }
1400{
1401 this->_query.searchCondition.inputBindings = &_boundInputs;
1402}
1403
1404template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1405size_t SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::CountImpl()
1406{
1407 auto stmt = SqlStatement { _dm.Connection() };
1408 stmt.Prepare(_formatter.SelectCount(this->_query.distinct,
1409 RecordTableName<Record>,
1410 this->_query.searchCondition.tableAlias,
1411 this->_query.searchCondition.tableJoins,
1412 this->_query.searchCondition.condition,
1413 this->_query.groupBy));
1414 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1415 if (reader.FetchRow())
1416 return reader.template GetColumn<size_t>(1);
1417 return 0;
1418}
1419
1420template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1421template <auto... RelationPath>
1422 requires DataMapperRecord<Record> && (sizeof...(RelationPath) >= 1)
1423Derived& SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::With()
1424{
1425 constexpr auto FirstField = detail::FirstOf<RelationPath...>;
1426
1427#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1428 static_assert(std::same_as<typename[:std::meta::parent_of(FirstField):], Record>,
1429 "The first relation named by With<>() must be a member of the record being queried");
1430#else
1431 // remove_cv_t: FirstField is a constexpr variable, so decltype() yields a *const* member pointer
1432 // type, which MemberClassType is not specialized for.
1433 static_assert(std::same_as<MemberClassType<std::remove_cv_t<decltype(FirstField)>>, Record>,
1434 "The first relation named by With<>() must be a member of the record being queried");
1435#endif
1436
1437 // A capture-less lambda, so this decays to a plain function pointer: the whole path is known at
1438 // compile time, so nothing has to be carried at run time beyond which instantiation to call.
1439 _relationPreloaders.emplace_back(+[](DataMapper& dm, std::span<Record* const> records) {
1440 [&]<auto Head, auto... Tail>() {
1441 dm.template PreloadRelationPath<Record, MemberIndexOf<Head>, Tail...>(records);
1442 }.template operator()<RelationPath...>();
1443 });
1444
1445 return static_cast<Derived&>(*this);
1446}
1447
1448template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1450{
1451 if constexpr (QueryOptions.eagerLoadDepth == 0)
1452 if (_relationPreloaders.empty())
1453 return;
1454
1455 if (records.empty())
1456 return;
1457
1458 auto pointers = std::vector<Record*> {};
1459 pointers.reserve(records.size());
1460 for (auto& record: records)
1461 pointers.emplace_back(&record);
1462 auto const batch = std::span<Record* const> { pointers };
1463
1464 // The named paths run first: the batched loaders skip a relation that is already loaded, so a
1465 // relation this query asked for by name is not fetched a second time by the depth walk below.
1466 for (auto const preloader: _relationPreloaders)
1467 preloader(_dm, batch);
1468
1469 if constexpr (QueryOptions.eagerLoadDepth > 0)
1470 _dm.template PreloadAllRelations<Record, QueryOptions.eagerLoadDepth>(batch);
1471}
1472
1473template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1474bool SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::ExistImpl()
1475{
1476 auto stmt = SqlStatement { _dm.Connection() };
1477
1478 auto const query = _formatter.SelectFirst(this->_query.distinct,
1479 _fields,
1480 RecordTableName<Record>,
1481 this->_query.searchCondition.tableAlias,
1482 this->_query.searchCondition.tableJoins,
1483 this->_query.searchCondition.condition,
1484 this->_query.orderBy,
1485 this->_query.groupBy,
1486 1);
1487
1488 stmt.Prepare(query);
1489 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
1490 return true;
1491 return false;
1492}
1493
1494template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1495void SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::DeleteImpl()
1496{
1497 auto stmt = SqlStatement { _dm.Connection() };
1498
1499 auto const query = _formatter.Delete(RecordTableName<Record>,
1500 this->_query.searchCondition.tableAlias,
1501 this->_query.searchCondition.tableJoins,
1502 this->_query.searchCondition.condition);
1503
1504 stmt.Prepare(query);
1505 [[maybe_unused]] auto cursor = stmt.ExecuteWithVariants(_boundInputs);
1506}
1507
1508template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1509std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl()
1510{
1511
1512 auto records = std::vector<Record> {};
1513 auto stmt = SqlStatement { _dm.Connection() };
1514 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1515 _fields,
1516 RecordTableName<Record>,
1517 this->_query.searchCondition.tableAlias,
1518 this->_query.searchCondition.tableJoins,
1519 this->_query.searchCondition.condition,
1520 this->_query.orderBy,
1521 this->_query.groupBy));
1522 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1523 if constexpr (DataMapperRecord<Record>)
1524 {
1525 // This can be called when record type is not plain aggregate type
1526 // but more complex tuple, like std::tuple<A, B>
1527 // for now we do not unwrap this type and just skip auto-loading configuration
1528 if constexpr (QueryOptions.loadRelations)
1529 {
1530 for (auto& record: records)
1531 {
1532 _dm.ConfigureRelationAutoLoading(record);
1533 }
1534 }
1535 RunRelationPreloaders(records);
1536 }
1537 return records;
1538}
1539
1540template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1541template <auto Field>
1542#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1543 requires(is_aggregate_type(parent_of(Field)))
1544#else
1545 requires std::is_member_object_pointer_v<decltype(Field)>
1546#endif
1547auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl() -> std::vector<ReferencedFieldTypeOf<Field>>
1548{
1549 using value_type = ReferencedFieldTypeOf<Field>;
1550 auto result = std::vector<value_type> {};
1551
1552 auto stmt = SqlStatement { _dm.Connection() };
1553 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1554 detail::FullyQualifiedNamesOf<Field>,
1555 RecordTableName<Record>,
1556 this->_query.searchCondition.tableAlias,
1557 this->_query.searchCondition.tableJoins,
1558 this->_query.searchCondition.condition,
1559 this->_query.orderBy,
1560 this->_query.groupBy));
1561 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1562 auto const outputColumnsBound = detail::CanSafelyBindOutputColumn<value_type>(stmt.Connection().ServerType());
1563 while (true)
1564 {
1565 auto& value = result.emplace_back();
1566 if (outputColumnsBound)
1567 reader.BindOutputColumn(1, &value);
1568
1569 if (!reader.FetchRow())
1570 {
1571 result.pop_back();
1572 break;
1573 }
1574
1575 if (!outputColumnsBound)
1576 value = reader.template GetColumn<value_type>(1);
1577 }
1578
1579 return result;
1580}
1581
1582template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1583template <auto... ReferencedFields>
1584 requires(sizeof...(ReferencedFields) >= 2)
1585auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::AllImpl() -> std::vector<Record>
1586{
1587 auto records = std::vector<Record> {};
1588 auto stmt = SqlStatement { _dm.Connection() };
1589
1590 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
1591 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1592 RecordTableName<Record>,
1593 this->_query.searchCondition.tableAlias,
1594 this->_query.searchCondition.tableJoins,
1595 this->_query.searchCondition.condition,
1596 this->_query.orderBy,
1597 this->_query.groupBy));
1598
1599 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1600 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1601 while (true)
1602 {
1603 auto& record = records.emplace_back();
1604 if (outputColumnsBound)
1605#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1606 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1607#else
1608 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1609#endif
1610 if (!reader.FetchRow())
1611 {
1612 records.pop_back();
1613 break;
1614 }
1615 if (!outputColumnsBound)
1616 {
1617 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1618 detail::GetAllColumns<ElementMask>(reader, record);
1619 }
1620 }
1621
1622 return records;
1623}
1624
1625template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1626std::optional<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl()
1627{
1628 std::optional<Record> record {};
1629 auto stmt = SqlStatement { _dm.Connection() };
1630 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1631 _fields,
1632 RecordTableName<Record>,
1633 this->_query.searchCondition.tableAlias,
1634 this->_query.searchCondition.tableJoins,
1635 this->_query.searchCondition.condition,
1636 this->_query.orderBy,
1637 this->_query.groupBy,
1638 1));
1639 Derived::ReadResult(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &record);
1640 if constexpr (QueryOptions.loadRelations)
1641 {
1642 if (record)
1643 _dm.ConfigureRelationAutoLoading(record.value());
1644 }
1645 if constexpr (DataMapperRecord<Record>)
1646 if (record)
1647 RunRelationPreloaders(std::span<Record> { &record.value(), 1 });
1648 return record;
1649}
1650
1651template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1652template <auto Field>
1653#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1654 requires(is_aggregate_type(parent_of(Field)))
1655#else
1656 requires std::is_member_object_pointer_v<decltype(Field)>
1657#endif
1658auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl() -> std::optional<ReferencedFieldTypeOf<Field>>
1659{
1660 auto constexpr count = 1;
1661 auto stmt = SqlStatement { _dm.Connection() };
1662 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1663 detail::FullyQualifiedNamesOf<Field>,
1664 RecordTableName<Record>,
1665 this->_query.searchCondition.tableAlias,
1666 this->_query.searchCondition.tableJoins,
1667 this->_query.searchCondition.condition,
1668 this->_query.orderBy,
1669 this->_query.groupBy,
1670 count));
1671 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
1672 return reader.template GetColumn<ReferencedFieldTypeOf<Field>>(1);
1673 return std::nullopt;
1674}
1675
1676template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1677template <auto... ReferencedFields>
1678 requires(sizeof...(ReferencedFields) >= 2)
1679auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl() -> std::optional<Record>
1680{
1681 auto optionalRecord = std::optional<Record> {};
1682
1683 auto stmt = SqlStatement { _dm.Connection() };
1684 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1685 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1686 RecordTableName<Record>,
1687 this->_query.searchCondition.tableAlias,
1688 this->_query.searchCondition.tableJoins,
1689 this->_query.searchCondition.condition,
1690 this->_query.orderBy,
1691 this->_query.groupBy,
1692 1));
1693
1694 auto& record = optionalRecord.emplace();
1695 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1696 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1697 if (outputColumnsBound)
1698#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1699 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1700#else
1701 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1702#endif
1703
1704 // A single return statement at the end is deliberate, not stylistic: a composite foreign key
1705 // configured below (ConfigureRelationAutoLoading) captures a pointer to *optionalRecord. An earlier
1706 // `return std::nullopt;` here defeats NRVO in both GCC and Clang (verified: it forces a
1707 // move-construct into the caller's storage at a new address), which would leave that captured
1708 // pointer dangling.
1709 if (reader.FetchRow())
1710 {
1711 if (!outputColumnsBound)
1712 {
1713 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1714 detail::GetAllColumns<ElementMask>(reader, record);
1715 }
1716
1717 if constexpr (QueryOptions.loadRelations)
1718 _dm.ConfigureRelationAutoLoading(record);
1719 }
1720 else
1721 {
1722 optionalRecord.reset();
1723 }
1724
1725 return optionalRecord;
1726}
1727
1728template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1729std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl(size_t n)
1730{
1731 auto records = std::vector<Record> {};
1732 auto stmt = SqlStatement { _dm.Connection() };
1733 records.reserve(n);
1734 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1735 _fields,
1736 RecordTableName<Record>,
1737 this->_query.searchCondition.tableAlias,
1738 this->_query.searchCondition.tableJoins,
1739 this->_query.searchCondition.condition,
1740 this->_query.orderBy,
1741 this->_query.groupBy,
1742 n));
1743 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1744
1745 if constexpr (QueryOptions.loadRelations)
1746 {
1747 for (auto& record: records)
1748 _dm.ConfigureRelationAutoLoading(record);
1749 }
1750 if constexpr (DataMapperRecord<Record>)
1751 RunRelationPreloaders(records);
1752 return records;
1753}
1754
1755template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1756std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RangeImpl(size_t offset, size_t limit)
1757{
1758 auto records = std::vector<Record> {};
1759 auto stmt = SqlStatement { _dm.Connection() };
1760 records.reserve(limit);
1761 stmt.Prepare(
1762 _formatter.SelectRange(this->_query.distinct,
1763 _fields,
1764 RecordTableName<Record>,
1765 this->_query.searchCondition.tableAlias,
1766 this->_query.searchCondition.tableJoins,
1767 this->_query.searchCondition.condition,
1768 !this->_query.orderBy.empty()
1769 ? this->_query.orderBy
1770 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1771 this->_query.groupBy,
1772 offset,
1773 limit));
1774 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1775 if constexpr (QueryOptions.loadRelations)
1776 {
1777 for (auto& record: records)
1778 _dm.ConfigureRelationAutoLoading(record);
1779 }
1780 if constexpr (DataMapperRecord<Record>)
1781 RunRelationPreloaders(records);
1782 return records;
1783}
1784
1785template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1786template <auto... ReferencedFields>
1787std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::RangeImpl(size_t offset, size_t limit)
1788{
1789 auto records = std::vector<Record> {};
1790 auto stmt = SqlStatement { _dm.Connection() };
1791 records.reserve(limit);
1792 stmt.Prepare(
1793 _formatter.SelectRange(this->_query.distinct,
1794 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1795 RecordTableName<Record>,
1796 this->_query.searchCondition.tableAlias,
1797 this->_query.searchCondition.tableJoins,
1798 this->_query.searchCondition.condition,
1799 !this->_query.orderBy.empty()
1800 ? this->_query.orderBy
1801 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1802 this->_query.groupBy,
1803 offset,
1804 limit));
1805
1806 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1807 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1808 while (true)
1809 {
1810 auto& record = records.emplace_back();
1811 if (outputColumnsBound)
1812#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1813 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1814#else
1815 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1816#endif
1817 if (!reader.FetchRow())
1818 {
1819 records.pop_back();
1820 break;
1821 }
1822 if (!outputColumnsBound)
1823 {
1824 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1825 detail::GetAllColumns<ElementMask>(reader, record);
1826 }
1827 }
1828
1829 if constexpr (QueryOptions.loadRelations)
1830 {
1831 for (auto& record: records)
1832 _dm.ConfigureRelationAutoLoading(record);
1833 }
1834
1835 return records;
1836}
1837
1838template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1839template <auto... ReferencedFields>
1840[[nodiscard]] std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::FirstImpl(size_t n)
1841{
1842 auto records = std::vector<Record> {};
1843 auto stmt = SqlStatement { _dm.Connection() };
1844 records.reserve(n);
1845 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1846 detail::FullyQualifiedNamesOf<ReferencedFields...>,
1847 RecordTableName<Record>,
1848 this->_query.searchCondition.tableAlias,
1849 this->_query.searchCondition.tableJoins,
1850 this->_query.searchCondition.condition,
1851 this->_query.orderBy,
1852 this->_query.groupBy,
1853 n));
1854
1855 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1856 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1857 while (true)
1858 {
1859 auto& record = records.emplace_back();
1860 if (outputColumnsBound)
1861#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1862 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1863#else
1864 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1865#endif
1866 if (!reader.FetchRow())
1867 {
1868 records.pop_back();
1869 break;
1870 }
1871 if (!outputColumnsBound)
1872 {
1873 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1874 detail::GetAllColumns<ElementMask>(reader, record);
1875 }
1876 }
1877
1878 if constexpr (QueryOptions.loadRelations)
1879 {
1880 for (auto& record: records)
1881 _dm.ConfigureRelationAutoLoading(record);
1882 }
1883
1884 return records;
1885}
1886
1887template <typename Record, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1888void SqlAllFieldsQueryBuilder<Record, QueryOptions, Execution>::ReadResults(SqlServerType sqlServerType,
1889 SqlResultCursor reader,
1890 std::vector<Record>* records)
1891{
1892 // Fast path: when every result column is a fixed-width row-bindable field and the driver honours
1893 // native row-array fetching, materialize the whole result set in row blocks (one SQLFetchScroll per
1894 // block) directly into records, instead of one SQLFetch round-trip per row. Results are identical to
1895 // the per-row path below; this only collapses ODBC round-trips (the win on high-latency links).
1896 if constexpr (detail::CanRowWiseFetchRecord<Record>())
1897 {
1898 if (detail::CanRowWiseFetchOn<Record>(sqlServerType))
1899 {
1900 detail::ReadAllRowWise(reader, records);
1901 return;
1902 }
1903 }
1904
1905 while (true)
1906 {
1907 Record& record = records->emplace_back();
1908 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1909 {
1910 records->pop_back();
1911 break;
1912 }
1913 }
1914}
1915
1916template <typename Record, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1917void SqlAllFieldsQueryBuilder<Record, QueryOptions, Execution>::ReadResult(SqlServerType sqlServerType,
1918 SqlResultCursor reader,
1919 std::optional<Record>* optionalRecord)
1920{
1921 Record& record = optionalRecord->emplace();
1922 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1923 optionalRecord->reset();
1924}
1925
1926template <typename FirstRecord, typename SecondRecord, DataMapperOptions QueryOptions, SqlQueryExecutionMode Execution>
1927void SqlAllFieldsQueryBuilder<std::tuple<FirstRecord, SecondRecord>, QueryOptions, Execution>::ReadResults(
1928 SqlServerType sqlServerType, SqlResultCursor reader, std::vector<RecordType>* records)
1929{
1930 // Fast path: a JOIN row of two row-bindable records is bound row-wise over the tuple and fetched in
1931 // blocks (one SQLFetchScroll per block) instead of one SQLFetch per row. Identical results.
1932 if constexpr (detail::CanRowWiseFetchTuple<FirstRecord, SecondRecord>())
1933 {
1934 if (detail::CanRowWiseFetchTupleOn<FirstRecord, SecondRecord>(sqlServerType))
1935 {
1936 detail::ReadAllRowWiseTuple<FirstRecord, SecondRecord>(reader, records);
1937 return;
1938 }
1939 }
1940
1941 while (true)
1942 {
1943 auto& record = records->emplace_back();
1944 auto& [firstRecord, secondRecord] = record;
1945
1946 using FirstRecordType = std::remove_cvref_t<decltype(firstRecord)>;
1947 using SecondRecordType = std::remove_cvref_t<decltype(secondRecord)>;
1948
1949 auto const outputColumnsBoundFirst = detail::CanSafelyBindOutputColumns<FirstRecordType>(sqlServerType);
1950 auto const outputColumnsBoundSecond = detail::CanSafelyBindOutputColumns<SecondRecordType>(sqlServerType);
1951 auto const canSafelyBindAll = outputColumnsBoundFirst && outputColumnsBoundSecond;
1952
1953 if (canSafelyBindAll)
1954 {
1955 detail::BindAllOutputColumnsWithOffset(reader, firstRecord, 1);
1956 // The second sub-record starts after the *columns* projected for the first one; relation
1957 // members are not projected, so RecordMemberCount would over-count here.
1958 detail::BindAllOutputColumnsWithOffset(
1959 reader, secondRecord, static_cast<SQLUSMALLINT>(1 + RecordColumnCount<FirstRecord>));
1960 }
1961
1962 if (!reader.FetchRow())
1963 {
1964 records->pop_back();
1965 break;
1966 }
1967
1968 if (!canSafelyBindAll)
1969 detail::GetAllColumns(reader, record);
1970 }
1971}
1972
1973template <typename Record>
1974std::string DataMapper::Inspect(Record const& record)
1975{
1976 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1977
1978 std::string str;
1979 Reflection::CallOnMembers(record, [&str]<typename Name, typename Value>(Name const& name, Value const& value) {
1980 if (!str.empty())
1981 str += '\n';
1982
1983 if constexpr (FieldWithStorage<Value>)
1984 {
1985 if constexpr (Value::IsOptional)
1986 {
1987 if (!value.Value().has_value())
1988 {
1989 str += std::format("{} {} := <nullopt>", Reflection::TypeNameOf<Value>, name);
1990 }
1991 else
1992 {
1993 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value().value());
1994 }
1995 }
1996 else if constexpr (IsBelongsTo<Value>)
1997 {
1998 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value());
1999 }
2000 else if constexpr (std::same_as<typename Value::ValueType, char>)
2001 {
2002 }
2003 else
2004 {
2005 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.InspectValue());
2006 }
2007 }
2008 else if constexpr (!IsHasMany<Value> && !IsHasManyThrough<Value> && !IsHasOneThrough<Value> && !IsBelongsTo<Value>
2009 && !IsCompositeForeignKey<Value>)
2010 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value);
2011 });
2012 return "{\n" + std::move(str) + "\n}";
2013}
2014
2015template <typename Record>
2016std::vector<std::string> DataMapper::CreateTableString(SqlServerType serverType)
2017{
2018 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2019
2020 auto migration = SqlQueryBuilder(*SqlQueryFormatter::Get(serverType)).Migration();
2021 auto createTable = migration.CreateTable(RecordTableName<Record>);
2022 detail::PopulateCreateTableBuilder<Record>(createTable);
2023 return migration.GetPlan().ToSql();
2024}
2025
2026template <typename FirstRecord, typename... MoreRecords>
2027std::vector<std::string> DataMapper::CreateTablesString(SqlServerType serverType)
2028{
2029 std::vector<std::string> output;
2030 auto const append = [&output](auto const& sql) {
2031 output.insert(output.end(), sql.begin(), sql.end());
2032 };
2033 append(CreateTableString<FirstRecord>(serverType));
2034 (append(CreateTableString<MoreRecords>(serverType)), ...);
2035 return output;
2036}
2037
2038template <typename Record>
2040{
2041 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2042
2043 ZoneScopedN("DataMapper::CreateTable");
2044 ZoneTextObject(RecordTableName<Record>);
2045
2046 auto const sqlQueryStrings = CreateTableString<Record>(_connection.ServerType());
2047 for (auto const& sqlQueryString: sqlQueryStrings) [[maybe_unused]]
2048 auto cursor = _stmt.ExecuteDirect(sqlQueryString);
2049}
2050
2051template <typename FirstRecord, typename... MoreRecords>
2053{
2054 CreateTable<FirstRecord>();
2055 (CreateTable<MoreRecords>(), ...);
2056}
2057
2058namespace detail
2059{
2060 /// Whether @p Record has at least one auto-maintained timestamp field (see @ref FieldTimestamp).
2061 template <typename Record>
2062 inline constexpr bool HasAutoTimestampFields =
2063 CheckFieldProperty<[]<typename FieldType>() { return IsAutoTimestampField<FieldType>; }, Record>;
2064
2065 /// Whether @p Record has at least one UpdatedAt timestamp field (see @ref FieldTimestamp).
2066 template <typename Record>
2067 inline constexpr bool HasUpdatedAtTimestampFields =
2068 CheckFieldProperty<[]<typename FieldType>() { return FieldTimestampOf<FieldType> == FieldTimestamp::UpdatedAt; },
2069 Record>;
2070} // namespace detail
2071
2072template <DataMapper::TimestampFill Fill, typename FieldType>
2073constexpr bool DataMapper::FillsTimestamp(FieldType const& field) noexcept
2074{
2075 if constexpr (!IsAutoTimestampField<FieldType>)
2076 return false;
2077 else if constexpr (Fill == TimestampFill::Unset)
2078 return detail::IsUnsetTimestamp(field.Value());
2079 else if constexpr (Fill == TimestampFill::All)
2080 return true;
2081 else if constexpr (Fill == TimestampFill::UpdatedAt)
2082 return FieldTimestampOf<FieldType> == FieldTimestamp::UpdatedAt;
2083 else
2084 return false;
2085}
2086
2087template <DataMapper::TimestampFill Fill, typename Record>
2088void DataMapper::StampTimestamps(Record& record, SqlDateTime const& now)
2089{
2090 EnumerateRecordMembers(record, [&now]<size_t I>(auto& field) {
2091 using MemberType = RecordMemberTypeOf<I, Record>;
2092 if constexpr (IsAutoTimestampField<MemberType>)
2093 {
2094 if (FillsTimestamp<Fill>(field))
2095 field = now;
2096 }
2097 });
2098}
2099
2100template <typename Record>
2101void DataMapper::StampTimestampsForUpdate(Record& record) const
2102{
2103 if constexpr (detail::HasUpdatedAtTimestampFields<Record>)
2104 {
2105 // An UpdatedAt field records a change to the row, so it only moves when something else is written.
2106 bool writesOtherColumn = false;
2107 EnumerateRecordMembers(record, [&writesOtherColumn]<size_t I>(auto const& field) {
2108 using MemberType = RecordMemberTypeOf<I, Record>;
2109 if constexpr (FieldWithStorage<MemberType> && FieldTimestampOf<MemberType> != FieldTimestamp::UpdatedAt)
2110 writesOtherColumn = writesOtherColumn || field.IsModified();
2111 });
2112
2113 auto const now = writesOtherColumn ? std::optional { CurrentTimestamp() } : std::nullopt;
2114 EnumerateRecordMembers(record, [&now]<size_t I>(auto& field) {
2115 using MemberType = RecordMemberTypeOf<I, Record>;
2116 if constexpr (FieldTimestampOf<MemberType> == FieldTimestamp::UpdatedAt)
2117 {
2118 // A value the caller assigned in this change is theirs to keep.
2119 if (field.IsModified() && !detail::IsUnsetTimestamp(field.Value()))
2120 return;
2121 if (now.has_value())
2122 field = *now;
2123 else
2124 field.SetModified(false); // Never write an unset timestamp, and keep a no-op update a no-op.
2125 }
2126 });
2127 }
2128}
2129
2130template <DataMapper::TimestampFill Fill, typename Record>
2131std::vector<Record> DataMapper::CopyWithTimestamps(std::span<Record const> records) const
2132{
2133 static_assert(std::copy_constructible<Record>,
2134 "Batch writes of a record with FieldTimestamp fields copy the batch to carry the generated "
2135 "timestamps, so the record type must be copy-constructible.");
2136
2137 auto copies = std::vector<Record>(records.begin(), records.end());
2138 auto const now = CurrentTimestamp();
2139 for (auto& copy: copies)
2140 StampTimestamps<Fill>(copy, now);
2141 return copies;
2142}
2143
2144template <typename Record>
2145std::optional<RecordPrimaryKeyType<Record>> DataMapper::GenerateAutoAssignPrimaryKey(Record const& record)
2146{
2147 // Auto-assignment produces exactly one value, and SetId() writes it into every generated primary
2148 // key member - so a record with several auto-assigned key members would silently receive the same
2149 // value in all of them. A composite key is declared PrimaryKey::Manual instead; see that enumerator.
2150 static_assert(detail::AutoAssignPrimaryKeyFieldCount<Record> <= 1,
2151 "A record may declare at most one auto-assigned primary key member. Auto-assignment yields a "
2152 "single value that would be written into every generated key member, so a composite key cannot "
2153 "be generated - declare its members PrimaryKey::Manual and set their values yourself before "
2154 "calling Create().");
2155
2156 std::optional<RecordPrimaryKeyType<Record>> result;
2158 record, [this, &result]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2159 if constexpr (IsField<PrimaryKeyType> && IsPrimaryKey<PrimaryKeyType>
2160 && detail::IsAutoAssignPrimaryKeyField<PrimaryKeyType>::value)
2161 {
2162 using ValueType = PrimaryKeyType::ValueType;
2163 if constexpr (std::same_as<ValueType, SqlGuid>)
2164 {
2165 if (!primaryKeyField.Value())
2166 [&](auto& res) {
2167 res.emplace(SqlGuid::Create());
2168 }(result);
2169 }
2170 else if constexpr (requires { ValueType {} + 1; })
2171 {
2172 if (primaryKeyField.Value() == ValueType {})
2173 {
2174 auto maxId = SqlStatement { _connection }.ExecuteDirectScalar<ValueType>(
2175 std::format(R"sql(SELECT MAX("{}") FROM "{}")sql",
2176 FieldNameAt<PrimaryKeyIndex, Record>,
2177 RecordTableName<Record>));
2178 result = maxId.value_or(ValueType {}) + 1;
2179 }
2180 }
2181 }
2182 });
2183 return result;
2184}
2185
2186template <DataMapper::PrimaryKeySource UsePkOverride, DataMapper::TimestampFill Fill, typename Record>
2187RecordPrimaryKeyType<Record> DataMapper::CreateInternal(
2188 Record const& record,
2189 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
2190 pkOverride)
2191{
2192 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2193
2194 auto query = _connection.Query(RecordTableName<Record>).Insert(nullptr);
2195
2196#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2197 constexpr auto ctx = std::meta::access_context::current();
2198 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2199 {
2200 using FieldType = typename[:std::meta::type_of(el):];
2201 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
2202 query.Set(FieldNameOf<el>, SqlWildcard);
2203 }
2204#else
2205 EnumerateRecordMembers(record, [&query]<auto I, typename FieldType>(FieldType const& /*field*/) {
2206 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
2207 query.Set(FieldNameAt<I, Record>, SqlWildcard);
2208 });
2209#endif
2210
2211 _stmt.Prepare(query);
2212
2213 // The time generated timestamps are inserted with: read once, and alive until the statement has run,
2214 // as the driver reads the bound parameter on execution.
2215 auto const now =
2216 Fill != TimestampFill::None && detail::HasAutoTimestampFields<Record> ? CurrentTimestamp() : SqlDateTime {};
2217
2218#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2219 int i = 1;
2220 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2221 {
2222 using FieldType = typename[:std::meta::type_of(el):];
2223 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
2224 {
2225 // The override replaces the generated member only; a PrimaryKey::Manual member is the caller's.
2226 if constexpr (detail::IsAutoAssignPrimaryKeyField<FieldType>::value
2227 && UsePkOverride == PrimaryKeySource::Override)
2228 _stmt.BindInputParameter(i++, *pkOverride, std::meta::identifier_of(el));
2229 else if (FillsTimestamp<Fill>(record.[:el:]))
2230 _stmt.BindInputParameter(i++, now, std::meta::identifier_of(el));
2231 else
2232 _stmt.BindInputParameter(i++, record.[:el:], std::meta::identifier_of(el));
2233 }
2234 }
2235#else
2236 Reflection::CallOnMembers(record,
2237 [this, &pkOverride, &now, i = SQLSMALLINT { 1 }]<typename Name, typename FieldType>(
2238 Name const& name, FieldType const& field) mutable {
2239 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
2240 {
2241 // The override replaces the generated member only; a Manual member is the caller's.
2242 if constexpr (detail::IsAutoAssignPrimaryKeyField<FieldType>::value
2243 && UsePkOverride == PrimaryKeySource::Override)
2244 _stmt.BindInputParameter(i++, *pkOverride, name);
2245 else if (FillsTimestamp<Fill>(field))
2246 _stmt.BindInputParameter(i++, now, name);
2247 else
2248 _stmt.BindInputParameter(i++, field, name);
2249 }
2250 });
2251#endif
2252 [[maybe_unused]] auto cursor = _stmt.Execute();
2253
2254 if constexpr (HasAutoIncrementPrimaryKey<Record>)
2255 // LastInsertId() returns size_t; a record whose auto-increment primary key is declared
2256 // narrower (e.g. int32_t, the common SQL Server `int IDENTITY` mapping) would otherwise hit
2257 // a narrowing conversion in brace-init, which is ill-formed and fails under warnings-as-errors
2258 // (MSVC C2397). The narrowing is deliberate here: the caller already chose that column's
2259 // width when declaring the record.
2260 return static_cast<RecordPrimaryKeyType<Record>>(_stmt.LastInsertId(RecordTableName<Record>));
2261 else if constexpr (HasPrimaryKey<Record>)
2262 {
2263 if constexpr (UsePkOverride == PrimaryKeySource::Override)
2264 return *pkOverride; // NOLINT(bugprone-unchecked-optional-access)
2265 else
2266 return RecordPrimaryKeyOf(record).Value();
2267 }
2268 else
2269 return {};
2270}
2271
2272template <typename Record>
2273RecordPrimaryKeyType<Record> DataMapper::CreateExplicit(Record const& record)
2274{
2275 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2276 return CreateInternal<PrimaryKeySource::Record, TimestampFill::Unset>(record);
2277}
2278
2279namespace detail
2280{
2281 /// @brief Whether member field type @p FieldType is an insertable column for a batched CREATE
2282 /// (bindable and not an auto-increment primary key). Single source of truth shared by the INSERT
2283 /// column-list builder and the value-accessor builder, so the bound `?` count and the accessor count
2284 /// cannot drift apart.
2285 template <typename FieldType>
2286 constexpr bool IsBatchInsertColumn = SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>;
2287
2288 /// @brief Whether @p FieldType is a SET column for a batched UPDATE (storable, non-primary-key).
2289 template <typename FieldType>
2290 constexpr bool IsBatchUpdateSetColumn = FieldWithStorage<FieldType> && !IsPrimaryKey<FieldType>;
2291
2292 /// @brief Whether @p FieldType is a WHERE (key) column for a batched UPDATE (a primary key).
2293 template <typename FieldType>
2294 constexpr bool IsBatchUpdateWhereColumn = IsPrimaryKey<FieldType>;
2295
2296 /// @brief Column accessor for batched DataMapper operations: maps a record to the value of its
2297 /// I-th member field, returning a reference so the native row-wise batch path binds it in place.
2298 template <std::size_t I>
2299 struct FieldValueAccessor
2300 {
2301 template <typename Record>
2302 decltype(auto) operator()(Record const& record) const
2303 {
2304 return GetRecordMemberAt<I>(record).Value();
2305 }
2306 };
2307
2308 /// Returns a one-element accessor tuple for member I when it is an insertable column (bindable and
2309 /// not an auto-increment primary key), or an empty tuple otherwise — to be flattened via tuple_cat.
2310 template <std::size_t I, typename Record>
2311 auto MakeCreateColumnAccessor()
2312 {
2313 using FieldType = RecordMemberTypeOf<I, Record>;
2314 if constexpr (IsBatchInsertColumn<FieldType>)
2315 return std::tuple<FieldValueAccessor<I>> {};
2316 else
2317 return std::tuple<> {};
2318 }
2319
2320 /// Accessor tuple for the SET clause of a batched UPDATE: storable, non-primary-key columns.
2321 template <std::size_t I, typename Record>
2322 auto MakeUpdateSetAccessor()
2323 {
2324 using FieldType = RecordMemberTypeOf<I, Record>;
2325 if constexpr (IsBatchUpdateSetColumn<FieldType>)
2326 return std::tuple<FieldValueAccessor<I>> {};
2327 else
2328 return std::tuple<> {};
2329 }
2330
2331 /// Accessor tuple for the WHERE clause of a batched UPDATE: primary-key columns.
2332 template <std::size_t I, typename Record>
2333 auto MakeUpdateWhereAccessor()
2334 {
2335 using FieldType = RecordMemberTypeOf<I, Record>;
2336 if constexpr (IsBatchUpdateWhereColumn<FieldType>)
2337 return std::tuple<FieldValueAccessor<I>> {};
2338 else
2339 return std::tuple<> {};
2340 }
2341} // namespace detail
2342
2343template <std::ranges::range Records>
2344void DataMapper::CreateAll(Records const& records)
2345{
2346 static_assert(std::ranges::contiguous_range<Records> && std::ranges::sized_range<Records>,
2347 "CreateAll requires a contiguous, sized range of records (e.g. std::vector, std::array, "
2348 "std::span, or a C array); native row-wise array binding needs the records laid out contiguously.");
2349 using Record = std::remove_cvref_t<std::ranges::range_value_t<Records>>;
2350 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2351
2352 ZoneScopedN("DataMapper::CreateAll");
2353 ZoneTextObject(RecordTableName<Record>);
2354
2355 if (std::ranges::empty(records))
2356 return;
2357
2358 auto const rows = std::span<Record const> { std::ranges::data(records), std::ranges::size(records) };
2359 if constexpr (detail::HasAutoTimestampFields<Record>)
2360 InsertBatch(std::span<Record const> { CopyWithTimestamps<TimestampFill::Unset>(rows) });
2361 else
2362 InsertBatch(rows);
2363}
2364
2365template <typename Record>
2366void DataMapper::InsertBatch(std::span<Record const> records)
2367{
2368 // Build the INSERT once, with the same column set and order as CreateInternal().
2369 auto query = _connection.Query(RecordTableName<Record>).Insert(nullptr);
2370 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
2371 if constexpr (detail::IsBatchInsertColumn<FieldType>)
2372 query.Set(FieldNameAt<I, Record>, SqlWildcard);
2373 });
2374 _stmt.Prepare(query);
2375
2376 // Build one value accessor per bound column (same filter/order) and submit the whole batch.
2377 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
2378 std::apply([&](auto const&... accessors) { std::ignore = _stmt.ExecuteBatch(records, accessors...); },
2379 std::tuple_cat(detail::MakeCreateColumnAccessor<Is, Record>()...));
2380 }(std::make_index_sequence<RecordMemberCount<Record>> {});
2381}
2382
2383template <DataMapperOptions QueryOptions, typename Record>
2384RecordPrimaryKeyType<Record> DataMapper::CreateCopyOf(Record const& originalRecord)
2385{
2386 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2387 static_assert(HasPrimaryKey<Record>, "CreateCopyOf requires a record type with a primary key");
2388 static_assert(
2389 HasAutoIncrementPrimaryKey<Record>
2390 || detail::CheckFieldProperty<[]<typename Field>() { return detail::IsAutoAssignPrimaryKeyField<Field>::value; },
2391 Record>,
2392 "CreateCopyOf() needs a primary key the mapper generates (AutoAssign or ServerSideAutoIncrement). A "
2393 "record keyed by PrimaryKey::Manual members only has no key to give the copy - set one on a copy "
2394 "of your own and call Create() instead.");
2395
2396 auto generatedKey = GenerateAutoAssignPrimaryKey(originalRecord);
2397 if (generatedKey)
2398 return CreateInternal<PrimaryKeySource::Override, TimestampFill::All>(originalRecord, generatedKey);
2399
2400 if constexpr (HasAutoIncrementPrimaryKey<Record>)
2401 return CreateInternal<PrimaryKeySource::Record, TimestampFill::All>(originalRecord);
2402
2403 return CreateInternal<PrimaryKeySource::Override, TimestampFill::All>(originalRecord, RecordPrimaryKeyType<Record> {});
2404}
2405
2406template <DataMapperOptions QueryOptions, typename Record>
2407RecordPrimaryKeyType<Record> DataMapper::Create(Record& record)
2408{
2409 static_assert(!std::is_const_v<Record>);
2410 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2411
2412 ZoneScopedN("DataMapper::Create");
2413 ZoneTextObject(RecordTableName<Record>);
2414
2415 auto generatedKey = GenerateAutoAssignPrimaryKey(record);
2416 if (generatedKey)
2417 SetId(record, *generatedKey);
2418
2419 // Stamp the record itself, so the caller sees the timestamps that were inserted.
2420 if constexpr (detail::HasAutoTimestampFields<Record>)
2421 StampTimestamps<TimestampFill::Unset>(record, CurrentTimestamp());
2422
2423 auto pk = CreateInternal<PrimaryKeySource::Record, TimestampFill::None>(record);
2424
2425 if constexpr (HasAutoIncrementPrimaryKey<Record>)
2426 SetId(record, pk);
2427
2428 SetModifiedState<ModifiedState::NotModified>(record);
2429
2430 if constexpr (QueryOptions.loadRelations)
2432
2433 if constexpr (HasPrimaryKey<Record>)
2434 return GetPrimaryKeyField(record);
2435}
2436
2437template <typename Record>
2438bool DataMapper::IsModified(Record const& record) const noexcept
2439{
2440 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2441
2442 bool modified = false;
2443
2444#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2445 auto constexpr ctx = std::meta::access_context::current();
2446 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2447 {
2448 if constexpr (requires { record.[:el:].IsModified(); })
2449 {
2450 modified = modified || record.[:el:].IsModified();
2451 }
2452 }
2453#else
2454 Reflection::CallOnMembers(record, [&modified](auto const& /*name*/, auto const& field) {
2455 if constexpr (requires { field.IsModified(); })
2456 {
2457 modified = modified || field.IsModified();
2458 }
2459 });
2460#endif
2461
2462 return modified;
2463}
2464
2465template <typename Record>
2466 requires HasPrimaryKey<Record>
2467void DataMapper::Update(Record& record)
2468{
2469 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2470
2471 ZoneScopedN("DataMapper::Update");
2472 ZoneTextObject(RecordTableName<Record>);
2473
2474 // Mark the UpdatedAt fields modified with the current time, so the SET clause below writes them.
2475 StampTimestampsForUpdate(record);
2476
2477 auto query = _connection.Query(RecordTableName<Record>).Update();
2478
2479 // Tracks whether the fold below contributed at least one assignment to the SET clause.
2480 // With no assignments the formatter would emit `UPDATE "T" SET WHERE ...`, which every
2481 // driver rejects as a syntax error, so we skip the statement entirely instead.
2482 bool anyFieldModified = false;
2483
2484#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2485 auto constexpr ctx = std::meta::access_context::current();
2486 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2487 {
2488 using FieldType = typename[:std::meta::type_of(el):];
2489 if constexpr (FieldWithStorage<FieldType>)
2490 {
2491 if (record.[:el:].IsModified())
2492 {
2493 query.Set(FieldNameOf<el>, SqlWildcard);
2494 anyFieldModified = true;
2495 }
2496 if constexpr (IsPrimaryKey<FieldType>)
2497 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
2498 }
2499 }
2500#else
2501 EnumerateRecordMembers(record, [&query, &anyFieldModified]<size_t I, typename FieldType>(FieldType const& field) {
2502 // for some reason compiler do not want to properly deduce FieldType, so here we
2503 // directly infer the type from the Record type and index
2504 using MemberType = RecordMemberTypeOf<I, Record>;
2505 // Relations (HasMany, HasManyThrough, HasOneThrough, ...) have no column of their own.
2506 if constexpr (FieldWithStorage<MemberType>)
2507 {
2508 if (field.IsModified())
2509 {
2510 query.Set(FieldNameAt<I, Record>, SqlWildcard);
2511 anyFieldModified = true;
2512 }
2513 if constexpr (IsPrimaryKey<MemberType>)
2514 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
2515 }
2516 });
2517#endif
2518
2519 // Nothing to write: an UPDATE with an empty SET clause is a no-op by definition.
2520 if (!anyFieldModified)
2521 return;
2522
2523 _stmt.Prepare(query);
2524
2525 SQLSMALLINT i = 1;
2526
2527#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2528 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2529 {
2530 using FieldType = typename[:std::meta::type_of(el):];
2531 // Relations (HasMany, HasManyThrough, HasOneThrough, ...) have no column of their own.
2532 if constexpr (FieldWithStorage<FieldType>)
2533 {
2534 if (record.[:el:].IsModified())
2535 {
2536 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
2537 }
2538 }
2539 }
2540
2541 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2542 {
2543 using FieldType = typename[:std::meta::type_of(el):];
2544 if constexpr (FieldWithStorage<FieldType>)
2545 {
2546 if constexpr (FieldType::IsPrimaryKey)
2547 {
2548 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
2549 }
2550 }
2551 }
2552#else
2553 // Bind the SET clause
2554 EnumerateRecordMembers(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
2555 // Relations (HasMany, HasManyThrough, HasOneThrough, ...) have no column of their own.
2557 {
2558 if (field.IsModified())
2559 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
2560 }
2561 });
2562
2563 // Bind the WHERE clause
2564 EnumerateRecordMembers(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
2565 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
2566 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
2567 });
2568#endif
2569
2570 [[maybe_unused]] auto cursor = _stmt.Execute();
2571
2572 SetModifiedState<ModifiedState::NotModified>(record);
2573}
2574
2575template <std::ranges::range Records>
2576void DataMapper::UpdateAll(Records const& records)
2577{
2578 static_assert(std::ranges::contiguous_range<Records> && std::ranges::sized_range<Records>,
2579 "UpdateAll requires a contiguous, sized range of records (e.g. std::vector, std::array, "
2580 "std::span, or a C array); native row-wise array binding needs the records laid out contiguously.");
2581 using Record = std::remove_cvref_t<std::ranges::range_value_t<Records>>;
2582 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2583 static_assert(HasPrimaryKey<Record>, "UpdateAll requires a record type with a primary key");
2584
2585 ZoneScopedN("DataMapper::UpdateAll");
2586 ZoneTextObject(RecordTableName<Record>);
2587
2588 if (std::ranges::empty(records))
2589 return;
2590
2591 auto const rows = std::span<Record const> { std::ranges::data(records), std::ranges::size(records) };
2592 if constexpr (detail::HasUpdatedAtTimestampFields<Record>)
2593 UpdateBatch(std::span<Record const> { CopyWithTimestamps<TimestampFill::UpdatedAt>(rows) });
2594 else
2595 UpdateBatch(rows);
2596}
2597
2598template <typename Record>
2599void DataMapper::UpdateBatch(std::span<Record const> records)
2600{
2601 // Build one UPDATE that writes all storable non-primary-key columns, matched on the primary key(s).
2602 auto query = _connection.Query(RecordTableName<Record>).Update();
2603 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
2604 if constexpr (detail::IsBatchUpdateSetColumn<FieldType>)
2605 query.Set(FieldNameAt<I, Record>, SqlWildcard);
2606 });
2607 EnumerateRecordMembers<Record>([&query]<auto I, typename FieldType>() {
2608 if constexpr (detail::IsBatchUpdateWhereColumn<FieldType>)
2609 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
2610 });
2611 _stmt.Prepare(query);
2612
2613 // Accessor order must match the SQL parameter order: SET columns first, then the WHERE key(s).
2614 [&]<std::size_t... Is>(std::index_sequence<Is...>) {
2615 std::apply([&](auto const&... accessors) { std::ignore = _stmt.ExecuteBatch(records, accessors...); },
2616 std::tuple_cat(detail::MakeUpdateSetAccessor<Is, Record>()...,
2617 detail::MakeUpdateWhereAccessor<Is, Record>()...));
2618 }(std::make_index_sequence<RecordMemberCount<Record>> {});
2619}
2620
2621template <typename Record>
2622std::size_t DataMapper::Delete(Record const& record)
2623{
2624 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2625
2626 ZoneScopedN("DataMapper::Delete");
2627 ZoneTextObject(RecordTableName<Record>);
2628
2629 auto query = _connection.Query(RecordTableName<Record>).Delete();
2630
2631#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2632 auto constexpr ctx = std::meta::access_context::current();
2633 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2634 {
2635 using FieldType = typename[:std::meta::type_of(el):];
2636 // Relations (HasMany, HasManyThrough, HasOneThrough, ...) have no column of their own.
2637 if constexpr (FieldWithStorage<FieldType>)
2638 if constexpr (FieldType::IsPrimaryKey)
2639 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
2640 }
2641#else
2642 EnumerateRecordMembers(record, [&query]<size_t I, typename FieldType>(FieldType const& /*field*/) {
2643 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
2644 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
2645 });
2646#endif
2647
2648 _stmt.Prepare(query);
2649
2650#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2651 SQLSMALLINT i = 1;
2652 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2653 {
2654 using FieldType = typename[:std::meta::type_of(el):];
2655 if constexpr (FieldWithStorage<FieldType>)
2656 {
2657 if constexpr (FieldType::IsPrimaryKey)
2658 {
2659 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
2660 }
2661 }
2662 }
2663#else
2664 // Bind the WHERE clause
2666 [this, i = SQLSMALLINT { 1 }]<size_t I, typename FieldType>(FieldType const& field) mutable {
2667 if constexpr (IsPrimaryKey<RecordMemberTypeOf<I, Record>>)
2668 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
2669 });
2670#endif
2671
2672 auto cursor = _stmt.Execute();
2673
2674 return cursor.NumRowsAffected();
2675}
2676
2677template <typename Record, DataMapperOptions QueryOptions, typename... PrimaryKeyTypes>
2678std::optional<Record> DataMapper::QuerySingle(PrimaryKeyTypes&&... primaryKeys)
2679{
2680 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2681
2682 ZoneScopedN("DataMapper::QuerySingle(PK)");
2683 ZoneTextObject(RecordTableName<Record>);
2684
2685 // Starter doesn't expose finalizers / Where until at least one column is
2686 // projected. The reflection enumeration below is constexpr-conditional, so
2687 // which iteration adds the first column isn't known up front — promote on the
2688 // first storage field via the returned Builder&, then reuse that pointer.
2689 auto selectStarter = _connection.Query(RecordTableName<Record>).Select();
2690 SqlSelectQueryBuilder* queryBuilder = nullptr;
2691 // Project exactly the members the read side (detail::ReadSingleResult) consumes as columns —
2692 // FieldWithStorage alone would drop plain bindable members such as `std::string note;`.
2693 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2694 if constexpr (RecordColumnMember<FieldType>)
2695 {
2696 if (queryBuilder == nullptr)
2697 queryBuilder = &selectStarter.Field(FieldNameAt<I, Record>);
2698 else
2699 queryBuilder->Field(FieldNameAt<I, Record>);
2700
2701 if constexpr (FieldWithStorage<FieldType>)
2702 {
2703 if constexpr (FieldType::IsPrimaryKey)
2704 std::ignore = queryBuilder->Where(FieldNameAt<I, Record>, SqlWildcard);
2705 }
2706 }
2707 });
2708
2709 _stmt.Prepare(queryBuilder->First());
2710 auto reader = _stmt.Execute(std::forward<PrimaryKeyTypes>(primaryKeys)...);
2711
2712 // A single return statement at the end is deliberate, not stylistic: a composite foreign key
2713 // configured below (ConfigureRelationAutoLoading) captures a pointer to *resultRecord. An earlier
2714 // `return std::nullopt;` here defeats NRVO in both GCC and Clang (verified: it forces a move-construct
2715 // into the caller's storage at a new address), which would leave that captured pointer dangling.
2716 auto resultRecord = std::optional<Record> { Record {} };
2717 if (detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
2718 {
2719 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
2720
2721 if constexpr (QueryOptions.loadRelations)
2722 ConfigureRelationAutoLoading(*resultRecord);
2723 }
2724 else
2725 {
2726 resultRecord.reset();
2727 }
2728
2729 return resultRecord;
2730}
2731
2732template <typename Record, typename... Args>
2733std::optional<Record> DataMapper::QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args)
2734{
2735 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2736
2737 ZoneScopedN("DataMapper::QuerySingle(Builder)");
2738 ZoneTextObject(RecordTableName<Record>);
2739
2740 // Same projection predicate as the read side; see the note in QuerySingle(PrimaryKeyTypes...).
2741 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
2742 if constexpr (RecordColumnMember<FieldType>)
2743 selectQuery.Field(SqlQualifiedTableColumnName { RecordTableName<Record>, FieldNameAt<I, Record> });
2744 });
2745 auto const composedSql = selectQuery.First().ToSql();
2746 ZoneTextObject(composedSql);
2747 _stmt.Prepare(composedSql);
2748 auto reader = _stmt.Execute(std::forward<Args>(args)...);
2749
2750 auto resultRecord = std::optional<Record> { Record {} };
2751 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
2752 return std::nullopt;
2753
2754 if (resultRecord)
2755 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
2756
2757 return resultRecord;
2758}
2759
2760// TODO: Provide Query(QueryBuilder, ...) method variant
2761
2762/// Queries multiple records from the database using a composed query and optional input parameters.
2763template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2764inline LIGHTWEIGHT_FORCE_INLINE std::vector<Record> DataMapper::Query(
2765 SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters)
2766{
2767 static_assert(DataMapperRecord<Record> || std::same_as<Record, SqlVariantRow>, "Record must satisfy DataMapperRecord");
2768
2769 ZoneScopedN("DataMapper::Query(ComposedQuery)");
2770 return Query<Record, QueryOptions>(selectQuery.ToSql(), std::forward<InputParameters>(inputParameters)...);
2771}
2772
2773template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2774std::vector<Record> DataMapper::Query(std::string_view sqlQueryString, InputParameters&&... inputParameters)
2775{
2776 ZoneScopedN("DataMapper::Query(string)");
2777 ZoneTextObject(sqlQueryString);
2778
2779 auto result = std::vector<Record> {};
2780 if constexpr (std::same_as<Record, SqlVariantRow>)
2781 {
2782 _stmt.Prepare(sqlQueryString);
2783 SqlResultCursor cursor = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2784 size_t const numResultColumns = cursor.NumColumnsAffected();
2785 while (cursor.FetchRow())
2786 {
2787 auto& record = result.emplace_back();
2788 record.reserve(numResultColumns);
2789 for (auto const i: std::views::iota(1U, numResultColumns + 1))
2790 record.emplace_back(cursor.GetColumn<SqlVariant>(static_cast<SQLUSMALLINT>(i)));
2791 }
2792 }
2793 else
2794 {
2795 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2796
2797 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
2798
2799 _stmt.Prepare(sqlQueryString);
2800 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2801
2802 for (;;)
2803 {
2804 auto& record = result.emplace_back();
2805
2806 if (canSafelyBindOutputColumns)
2807 BindOutputColumns(record, reader);
2808
2809 if (!reader.FetchRow())
2810 break;
2811
2812 if (!canSafelyBindOutputColumns)
2813 detail::GetAllColumns(reader, record);
2814 }
2815
2816 // Drop the last record, which we failed to fetch (End of result set).
2817 result.pop_back();
2818
2819 for (auto& record: result)
2820 {
2821 SetModifiedState<ModifiedState::NotModified>(record);
2822 if constexpr (QueryOptions.loadRelations)
2824 }
2825 }
2826
2827 return result;
2828}
2829
2830template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions>
2831 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
2832std::vector<std::tuple<First, Second, Rest...>> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery)
2833{
2834 using value_type = std::tuple<First, Second, Rest...>;
2835 auto result = std::vector<value_type> {};
2836
2837 ZoneScopedN("DataMapper::Query(ComposedQuery -> tuple)");
2838 auto const tupleSql = selectQuery.ToSql();
2839 ZoneTextObject(tupleSql);
2840 _stmt.Prepare(tupleSql);
2841 auto reader = _stmt.Execute();
2842
2843 // The 1-based result set index of the first column belonging to the I-th sub-record. The projection
2844 // (SqlSelectQueryBuilder::Fields<Records...>) emits one entry per RecordColumnMember, so relation
2845 // members contribute no column and RecordMemberCount would over-count the preceding sub-records.
2846 constexpr auto calculateOffset = []<size_t I, typename Tuple>() {
2847 size_t offset = 1;
2848
2849 if constexpr (I > 0)
2850 {
2851 [&]<size_t... Indices>(std::index_sequence<Indices...>) {
2852 ((Indices < I ? (offset += RecordColumnCount<std::tuple_element_t<Indices, Tuple>>) : 0), ...);
2853 }(std::make_index_sequence<I> {});
2854 }
2855 return offset;
2856 };
2857
2858 auto const BindElements = [&](auto& record) {
2859 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2860 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
2861 auto& element = std::get<I>(record);
2862 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
2863 this->BindOutputColumns<TupleElement, offset>(element, reader);
2864 });
2865 };
2866
2867 auto const GetElements = [&](auto& record) {
2868 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2869 auto& element = std::get<I>(record);
2870 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
2871 detail::GetAllColumns(reader, element, offset - 1);
2872 });
2873 };
2874
2875 bool const canSafelyBindOutputColumns = [&]() {
2876 bool result = true;
2877 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2878 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
2879 result &= detail::CanSafelyBindOutputColumns<TupleElement>(_stmt.Connection().ServerType());
2880 });
2881 return result;
2882 }();
2883
2884 for (;;)
2885 {
2886 auto& record = result.emplace_back();
2887
2888 if (canSafelyBindOutputColumns)
2889 BindElements(record);
2890
2891 if (!reader.FetchRow())
2892 break;
2893
2894 if (!canSafelyBindOutputColumns)
2895 GetElements(record);
2896 }
2897
2898 // Drop the last record, which we failed to fetch (End of result set).
2899 result.pop_back();
2900
2901 for (auto& record: result)
2902 {
2903 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
2904 auto& element = std::get<I>(record);
2905 SetModifiedState<ModifiedState::NotModified>(element);
2906 if constexpr (QueryOptions.loadRelations)
2907 {
2909 }
2910 });
2911 }
2912
2913 return result;
2914}
2915
2916template <typename ElementMask, typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
2917std::vector<Record> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery,
2918 InputParameters&&... inputParameters)
2919{
2920 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2921
2922 ZoneScopedN("DataMapper::Query(ComposedQuery, ElementMask)");
2923 auto const maskedSql = selectQuery.ToSql();
2924 ZoneTextObject(maskedSql);
2925 _stmt.Prepare(maskedSql);
2926
2927 auto records = std::vector<Record> {};
2928
2929 // TODO: We could optimize this further by only considering ElementMask fields in Record.
2930 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
2931
2932 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
2933
2934 for (;;)
2935 {
2936 auto& record = records.emplace_back();
2937
2938 if (canSafelyBindOutputColumns)
2939 BindOutputColumns<ElementMask>(record, reader);
2940
2941 if (!reader.FetchRow())
2942 break;
2943
2944 if (!canSafelyBindOutputColumns)
2945 detail::GetAllColumns<ElementMask>(reader, record);
2946 }
2947
2948 // Drop the last record, which we failed to fetch (End of result set).
2949 records.pop_back();
2950
2951 for (auto& record: records)
2952 {
2953 SetModifiedState<ModifiedState::NotModified>(record);
2954 if constexpr (QueryOptions.loadRelations)
2956 }
2957
2958 return records;
2959}
2960
2961template <DataMapper::ModifiedState state, typename Record>
2962void DataMapper::SetModifiedState(Record& record) noexcept
2963{
2964 static_assert(!std::is_const_v<Record>);
2965 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2966
2967 EnumerateRecordMembers(record, []<size_t I, typename FieldType>(FieldType& field) {
2968 if constexpr (requires { field.SetModified(false); })
2969 {
2970 if constexpr (state == ModifiedState::Modified)
2971 field.SetModified(true);
2972 else
2973 field.SetModified(false);
2974 }
2975 });
2976}
2977
2978template <typename Record, typename Callable>
2979inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Record& record, Callable const& callable)
2980{
2981 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2982
2983 EnumerateRecordMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
2984 if constexpr (IsField<FieldType>)
2985 {
2986 if constexpr (FieldType::IsPrimaryKey)
2987 {
2988 return callable.template operator()<I, FieldType>(field);
2989 }
2990 }
2991 });
2992}
2993
2994template <typename Record, typename Callable>
2995inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Callable const& callable)
2996{
2997 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2998
2999 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
3000 if constexpr (IsField<FieldType>)
3001 {
3002 if constexpr (FieldType::IsPrimaryKey)
3003 {
3004 return callable.template operator()<I, FieldType>();
3005 }
3006 }
3007 });
3008}
3009
3010template <typename Record, typename Callable>
3011inline LIGHTWEIGHT_FORCE_INLINE void CallOnBelongsTo(Callable const& callable)
3012{
3013 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3014
3015 EnumerateRecordMembers<Record>([&]<size_t I, typename FieldType>() {
3016 if constexpr (IsBelongsTo<FieldType>)
3017 {
3018 return callable.template operator()<I, FieldType>();
3019 }
3020 });
3021}
3022
3023template <typename FieldType>
3024std::shared_ptr<typename FieldType::ReferencedRecord> DataMapper::LoadCompositeForeignKeyRecord(
3025 FieldType::OrderedValueType const& keys)
3026{
3027 using ReferencedRecord = FieldType::ReferencedRecord;
3028
3029 auto loaded =
3030 std::apply([this](auto const&... key) { return this->template QuerySingle<ReferencedRecord>(key...); }, keys);
3031 if (!loaded)
3032 return {};
3033 return std::make_shared<ReferencedRecord>(std::move(*loaded));
3034}
3035
3036template <typename Record, typename FieldType>
3037void DataMapper::LoadCompositeForeignKey(Record const& record, FieldType& field)
3038{
3039 using ReferencedRecord = FieldType::ReferencedRecord;
3040
3041 ZoneScopedN("DataMapper::LoadCompositeForeignKey");
3042 ZoneTextObject(RecordTableName<ReferencedRecord>);
3043
3044 // OrderedValuesOf() rather than ValuesOf(): QuerySingle emits one WHERE predicate per primary key
3045 // member of the referenced record, in that record's member declaration order, and binds its
3046 // arguments positionally - so the values have to be permuted into that order first. See
3047 // CompositeKeyOrderingTests.cpp.
3048 auto loaded = LoadCompositeForeignKeyRecord<FieldType>(FieldType::OrderedValuesOf(record));
3049
3050 // A missing target row leaves the relation unloaded rather than throwing here: eagerly loading a
3051 // dangling foreign key is a data-integrity problem to surface at the accessor, which is where the
3052 // lazy path reports it too.
3053 if (!loaded)
3054 {
3056 std::format("Loading composite foreign key failed for {}", RecordTableName<ReferencedRecord>));
3057 return;
3058 }
3059
3060 field.EmplaceRecord(std::move(loaded));
3061}
3062
3063template <typename FieldType>
3064std::optional<typename FieldType::ReferencedRecord> DataMapper::LoadBelongsTo(FieldType::ValueType value)
3065{
3066 using ReferencedRecord = FieldType::ReferencedRecord;
3067
3068 ZoneScopedN("DataMapper::LoadBelongsTo");
3069 ZoneTextObject(RecordTableName<ReferencedRecord>);
3070
3071 std::optional<ReferencedRecord> record { std::nullopt };
3072
3073 // A NULL foreign key references nothing - that is the relation being empty, not a failed load.
3074 if constexpr (FieldType::IsOptional)
3075 if (!value.has_value())
3076 return record;
3077
3078#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
3079 auto constexpr ctx = std::meta::access_context::current();
3080 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^ReferencedRecord, ctx)))
3081 {
3082 using BelongsToFieldType = typename[:std::meta::type_of(el):];
3083 if constexpr (IsField<BelongsToFieldType>)
3084 if constexpr (BelongsToFieldType::IsPrimaryKey)
3085 {
3086 if (auto result = QuerySingle<ReferencedRecord>(value); result)
3087 record = std::move(result);
3088 else
3090 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
3091 }
3092 }
3093#else
3094 CallOnPrimaryKey<ReferencedRecord>([&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>() {
3095 if (auto result = QuerySingle<ReferencedRecord>(value); result)
3096 record = std::move(result);
3097 else
3099 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
3100 });
3101#endif
3102 return record;
3103}
3104
3105template <typename Record, typename OtherRecord, auto InverseSelector, typename Callable>
3106void DataMapper::CallOnHasMany(Record& record, Callable const& callback)
3107{
3108 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3109 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
3110
3111 using FieldType = HasMany<OtherRecord, InverseSelector>;
3112 using ReferencedRecord = FieldType::ReferencedRecord;
3113
3114 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
3115 auto query = BuildHasManySelectQuery<Record, ReferencedRecord, InverseSelector>();
3116 callback(query, primaryKeyField);
3117 });
3118}
3119
3120template <typename OwnerRecord, typename OtherRecord, auto InverseSelector>
3121SqlSelectQueryBuilder DataMapper::BuildHasManySelectQuery()
3122{
3123 return BuildRecordSelectQuery<OtherRecord>()
3124 .Where(InverseBelongsToFieldNameOf<OwnerRecord, OtherRecord, InverseSelector>, SqlWildcard)
3125 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<OtherRecord>, OtherRecord>);
3126}
3127
3128template <typename Record, typename OtherRecord, auto InverseSelector>
3129void DataMapper::LoadHasMany(Record& record, HasMany<OtherRecord, InverseSelector>& field)
3130{
3131 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3132 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
3133
3134 ZoneScopedN("DataMapper::LoadHasMany");
3135 ZoneTextObject(RecordTableName<OtherRecord>);
3136
3137 CallOnHasMany<Record, OtherRecord, InverseSelector>(
3138 record, [&](SqlSelectQueryBuilder selectQuery, auto& primaryKeyField) {
3139 field.Emplace(detail::ToSharedPtrList(Query<OtherRecord>(selectQuery.All(), primaryKeyField.Value())));
3140 });
3141}
3142
3143namespace detail
3144{
3145 /// @brief Invokes @p callable once per chunk of at most @p chunkSize consecutive elements of @p values.
3146 ///
3147 /// Used to keep a generated `WHERE ... IN (...)` predicate inside what the dialect accepts (see
3148 /// @ref SqlQueryFormatter::MaxInPredicateValues) while still resolving a whole batch in a number of
3149 /// queries that depends on the chunk size, not on the number of records.
3150 ///
3151 /// @param values The values to split.
3152 /// @param chunkSize Maximum number of values per chunk; zero is treated as "all in one chunk".
3153 /// @param callable Invoked with a `std::span` over each chunk, in order.
3154 template <typename T, typename Callable>
3155 void ForEachChunk(std::span<T const> values, size_t chunkSize, Callable const& callable)
3156 {
3157 auto const stride = chunkSize == 0 ? values.size() : chunkSize;
3158 for (size_t offset = 0; offset < values.size(); offset += stride)
3159 callable(values.subspan(offset, (std::min) (stride, values.size() - offset)));
3160 }
3161} // namespace detail
3162
3163template <typename Record>
3164SqlSelectQueryBuilder DataMapper::BuildRecordSelectQuery()
3165{
3166 return _connection.Query(RecordTableName<Record>).Select().Build([](auto& query) {
3167 EnumerateRecordMembers<Record>([&query]<size_t I, typename FieldType>() {
3168 if constexpr (FieldWithStorage<FieldType>)
3169 query.Field(FieldNameAt<I, Record>);
3170 });
3171 });
3172}
3173
3174template <typename Record, size_t FieldIndex>
3175void DataMapper::PreloadRelation(std::span<Record* const> records)
3176{
3177 using FieldType = RecordMemberTypeOf<FieldIndex, Record>;
3178
3179 static_assert(IsBelongsTo<FieldType> || IsHasMany<FieldType>,
3180 "Eager loading via With<>() is supported for BelongsTo and HasMany relations. "
3181 "HasOneThrough, HasManyThrough and CompositeForeignKey still load on demand.");
3182
3183 // Defensive rather than reachable: every caller already returns on an empty batch
3184 // (RunRelationPreloaders, PreloadAllRelations, and the `targets.empty()` guards in
3185 // PreloadRelationPath / PreloadAllRelations). Kept so a future caller cannot trip over it, which
3186 // is also why coverage reports never mark this line.
3187 if (records.empty())
3188 return;
3189
3190 if constexpr (IsBelongsTo<FieldType>)
3191 PreloadBelongsTo<Record, FieldIndex>(records);
3192 else if constexpr (IsHasMany<FieldType>)
3193 PreloadHasMany<Record, FieldIndex>(records);
3194}
3195
3196template <typename Record, size_t FieldIndex>
3197auto DataMapper::CollectRelationTargets(std::span<Record* const> records)
3198{
3199 using FieldType = RecordMemberTypeOf<FieldIndex, Record>;
3200 using TargetRecord = FieldType::ReferencedRecord;
3201
3202 auto targets = std::vector<TargetRecord*> {};
3203 targets.reserve(records.size());
3204
3205 for (auto* record: records)
3206 {
3207 auto& field = GetRecordMemberAt<FieldIndex>(*record);
3208 if constexpr (IsBelongsTo<FieldType>)
3209 {
3210 // LoadedRecord(), not Record(): asking through the accessor would run the on-demand
3211 // loader for exactly the rows the batch failed to resolve, one query at a time.
3212 if (auto* target = field.LoadedRecord(); target != nullptr)
3213 targets.emplace_back(target);
3214 }
3215 else if constexpr (IsHasMany<FieldType>)
3216 {
3217 if (auto* loaded = field.LoadedRecords(); loaded != nullptr)
3218 for (auto& element: *loaded)
3219 targets.emplace_back(element.get());
3220 }
3221 }
3222
3223 return targets;
3224}
3225
3226template <typename Record, size_t FieldIndex, auto... RestOfPath>
3227void DataMapper::PreloadRelationPath(std::span<Record* const> records)
3228{
3229 PreloadRelation<Record, FieldIndex>(records);
3230
3231 if constexpr (sizeof...(RestOfPath) > 0)
3232 {
3233 using FieldType = RecordMemberTypeOf<FieldIndex, Record>;
3234 using TargetRecord = FieldType::ReferencedRecord;
3235
3236 auto targets = CollectRelationTargets<Record, FieldIndex>(records);
3237 if (targets.empty())
3238 return;
3239
3240 // Every owner holds its own copy of a BelongsTo target, so the same underlying row can
3241 // appear many times here. That is not wasted work: the level below deduplicates the keys
3242 // before querying, so the row is still fetched once and then handed to each copy.
3243 // The static_assert below is what keeps the path honest: without it, a path element naming a
3244 // member of some *other* record still compiles, because MemberIndexOf<> yields that member's
3245 // index within its own class, which is then read as an index into TargetRecord - silently
3246 // eager-loading whatever relation happens to sit at the same position (say
3247 // `With<&Track::album, &Track::genre>()` resolving Album's member 2).
3248 [&]<auto NextField, auto... Rest>() {
3249#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
3250 static_assert(std::same_as<typename[:std::meta::parent_of(NextField):], TargetRecord>,
3251 "Each relation named after the first in With<>() must be a member of the "
3252 "preceding relation's referenced record");
3253#else
3254 static_assert(std::same_as<MemberClassType<std::remove_cv_t<decltype(NextField)>>, TargetRecord>,
3255 "Each relation named after the first in With<>() must be a member of the "
3256 "preceding relation's referenced record");
3257#endif
3258 PreloadRelationPath<TargetRecord, MemberIndexOf<NextField>, Rest...>(std::span<TargetRecord* const> { targets });
3259 }.template operator()<RestOfPath...>();
3260 }
3261}
3262
3263template <typename Record, size_t Depth>
3264void DataMapper::PreloadAllRelations(std::span<Record* const> records)
3265{
3266 if constexpr (Depth == 0)
3267 return;
3268 else
3269 {
3270 // As in PreloadRelation: both callers (RunRelationPreloaders and the recursive descent below)
3271 // already guard against an empty batch, so this is belt-and-braces and stays uncovered.
3272 if (records.empty())
3273 return;
3274
3275 Reflection::template_for<0, RecordMemberCount<Record>>([&]<auto I>() {
3276 using FieldType = RecordMemberTypeOf<I, Record>;
3277 // Relations the batched loaders do not cover are left to their on-demand loaders rather
3278 // than failing the whole query: this option means "load what can be loaded in bulk".
3279 if constexpr (IsBelongsTo<FieldType> || IsHasMany<FieldType>)
3280 {
3281 PreloadRelation<Record, I>(records);
3282
3283 if constexpr (Depth > 1)
3284 {
3285 using TargetRecord = FieldType::ReferencedRecord;
3286 auto targets = CollectRelationTargets<Record, I>(records);
3287 if (!targets.empty())
3288 PreloadAllRelations<TargetRecord, Depth - 1>(std::span<TargetRecord* const> { targets });
3289 }
3290 }
3291 });
3292 }
3293}
3294
3295template <typename Record, size_t FieldIndex>
3296void DataMapper::PreloadBelongsTo(std::span<Record* const> records)
3297{
3298 using FieldType = RecordMemberTypeOf<FieldIndex, Record>;
3299 using ReferencedRecord = FieldType::ReferencedRecord;
3300 using KeyType = RecordPrimaryKeyType<ReferencedRecord>;
3301
3302 static_assert(HasPrimaryKey<ReferencedRecord>,
3303 "Eager loading a BelongsTo requires the referenced record to have a primary key");
3304
3305 ZoneScopedN("DataMapper::PreloadBelongsTo");
3306 ZoneTextObject(RecordTableName<ReferencedRecord>);
3307
3308 auto const primaryKeyOf = [](ReferencedRecord const& record) {
3309 return GetPrimaryKeyField(record);
3310 };
3311
3312 // The distinct, non-NULL foreign keys of the batch. Sorted and deduplicated rather than hashed:
3313 // ordering is all a key column type has to provide, while std::hash is not specialized for every
3314 // one of them. The deduplication is what makes this cheaper than the row count - many rows
3315 // commonly point at the same parent, which is then fetched once instead of once per row.
3316 auto keys = std::vector<KeyType> {};
3317 keys.reserve(records.size());
3318 for (auto const* record: records)
3319 {
3320 auto const& field = GetRecordMemberAt<FieldIndex>(*record);
3321 // Already resolved - by a named With<>() path, or by an outer level of the depth walk. Asking
3322 // again would be a second query for a row that is already in memory.
3323 if (field.LoadedRecord() != nullptr)
3324 continue;
3325
3326 auto const& value = field.Value();
3327 if constexpr (FieldType::IsOptional)
3328 {
3329 if (value.has_value())
3330 keys.emplace_back(*value);
3331 }
3332 else
3333 keys.emplace_back(value);
3334 }
3335 std::ranges::sort(keys);
3336 keys.erase(std::ranges::unique(keys).begin(), keys.end());
3337 if (keys.empty())
3338 return;
3339
3340 auto loaded = std::vector<ReferencedRecord> {};
3341 loaded.reserve(keys.size());
3342 detail::ForEachChunk(std::span<KeyType const> { keys },
3343 _connection.QueryFormatter().MaxInPredicateValues(),
3344 [&](std::span<KeyType const> chunk) {
3345 auto selectQuery = BuildRecordSelectQuery<ReferencedRecord>().WhereIn(
3346 FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord>, chunk);
3347 auto rows = Query<ReferencedRecord>(selectQuery.All());
3348 loaded.insert(
3349 loaded.end(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end()));
3350 });
3351
3352 std::ranges::sort(loaded, {}, primaryKeyOf);
3353
3354 for (auto* record: records)
3355 {
3356 auto& field = GetRecordMemberAt<FieldIndex>(*record);
3357 // Only reachable from a batch mixing already-loaded and unloaded records: the key-collection
3358 // loop above skips the loaded ones, and a batch in which *every* record is loaded has already
3359 // returned at the `keys.empty()` check. No current call path builds such a mix, so this arm is
3360 // not covered by the suite.
3361 if (field.LoadedRecord() != nullptr)
3362 continue;
3363
3364 auto const& value = field.Value();
3365 if constexpr (FieldType::IsOptional)
3366 if (!value.has_value())
3367 continue;
3368
3369 auto const key = [&] {
3370 if constexpr (FieldType::IsOptional)
3371 return *value;
3372 else
3373 return value;
3374 }();
3375
3376 auto const it = std::ranges::lower_bound(loaded, key, {}, primaryKeyOf);
3377 // A foreign key pointing at a row that is not there is left unloaded rather than adopted as
3378 // empty: a missing target row is a data-integrity problem for the accessor to surface, which
3379 // is also what the on-demand path does.
3380 if (it != loaded.end() && primaryKeyOf(*it) == key)
3381 field.AdoptFetchedRecord(*it);
3382 }
3383}
3384
3385template <typename Record, size_t FieldIndex>
3386void DataMapper::PreloadHasMany(std::span<Record* const> records)
3387{
3388 using FieldType = RecordMemberTypeOf<FieldIndex, Record>;
3389 using ReferencedRecord = FieldType::ReferencedRecord;
3390 using ReferencedRecordList = FieldType::ReferencedRecordList;
3391 using KeyType = RecordPrimaryKeyType<Record>;
3392
3393 static_assert(HasPrimaryKey<Record>, "Eager loading a HasMany requires the owning record to have a primary key");
3394
3395 ZoneScopedN("DataMapper::PreloadHasMany");
3396 ZoneTextObject(RecordTableName<ReferencedRecord>);
3397
3398 // Every fetched child carries its owner's key in the BelongsTo that backs this relation; that is
3399 // what assigns the row to an owner below, without a second query to find out.
3400 constexpr size_t InverseIndex = InverseBelongsToIndexOf<Record, ReferencedRecord, FieldType::InverseSelector>;
3401
3402 auto keys = std::vector<KeyType> {};
3403 keys.reserve(records.size());
3404 for (auto* record: records)
3405 // See PreloadBelongsTo: a list already in memory is not fetched again.
3406 if (GetRecordMemberAt<FieldIndex>(*record).LoadedRecords() == nullptr)
3407 keys.emplace_back(GetPrimaryKeyField(*record));
3408 std::ranges::sort(keys);
3409 keys.erase(std::ranges::unique(keys).begin(), keys.end());
3410 if (keys.empty())
3411 return;
3412
3413 // One bucket per distinct owner key, in the same sorted order, so a child row finds its owner by
3414 // binary search rather than by scanning the batch (which would be quadratic in the row count).
3415 auto buckets = std::vector<ReferencedRecordList> {};
3416 buckets.resize(keys.size());
3417
3418 detail::ForEachChunk(
3419 std::span<KeyType const> { keys },
3420 _connection.QueryFormatter().MaxInPredicateValues(),
3421 [&](std::span<KeyType const> chunk) {
3422 auto selectQuery =
3423 BuildRecordSelectQuery<ReferencedRecord>()
3424 .WhereIn(InverseBelongsToFieldNameOf<Record, ReferencedRecord, FieldType::InverseSelector>, chunk)
3425 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord>);
3426 for (auto& child: Query<ReferencedRecord>(selectQuery.All()))
3427 {
3428 // The inverse BelongsTo holds the owner's key, or an optional of it when the foreign
3429 // key is nullable; a NULL there is a child no owner in the batch can claim.
3430 std::optional<KeyType> const ownerKey = GetRecordMemberAt<InverseIndex>(child).Value();
3431 if (!ownerKey.has_value())
3432 continue;
3433
3434 auto const it = std::ranges::lower_bound(keys, *ownerKey);
3435 if (it != keys.end() && *it == *ownerKey)
3436 buckets[static_cast<size_t>(std::distance(keys.begin(), it))].emplace_back(
3437 std::make_shared<ReferencedRecord>(std::move(child)));
3438 }
3439 });
3440
3441 for (auto* record: records)
3442 {
3443 auto& field = GetRecordMemberAt<FieldIndex>(*record);
3444 // See PreloadBelongsTo: reachable only from a batch mixing loaded and unloaded records, which
3445 // no current call path produces - a wholly-loaded batch returns at the `keys.empty()` check.
3446 if (field.LoadedRecords() != nullptr)
3447 continue;
3448
3449 // Likewise unreachable today: every record that got past the check above contributed its key
3450 // to `keys` in the collection loop, so the lookup cannot miss. Kept as a guard for a future
3451 // caller that hands in a batch whose keys were filtered elsewhere.
3452 auto const it = std::ranges::lower_bound(keys, GetPrimaryKeyField(*record));
3453 if (it == keys.end() || !(*it == GetPrimaryKeyField(*record)))
3454 continue;
3455
3456 // Owners with no children are emplaced empty on purpose: leaving the relation unloaded would
3457 // send the very first access back to the database for a result already known to be empty.
3458 auto& bucket = buckets[static_cast<size_t>(std::distance(keys.begin(), it))];
3459 field.Emplace(ReferencedRecordList { bucket });
3460 }
3461}
3462
3463template <typename ReferencedRecord, typename ThroughRecord, typename Record, auto OwnerSelector, auto ThroughSelector>
3464SqlSelectQueryBuilder DataMapper::BuildHasOneThroughSelectQuery()
3465{
3466 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3467 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
3468
3469 // The foreign key of ThroughRecord pointing at the record owning this relationship.
3470 constexpr size_t ThroughToOwnerIndex = InverseBelongsToIndexOf<Record, ThroughRecord, OwnerSelector>;
3471
3472 // The foreign key of ReferencedRecord pointing at ThroughRecord.
3473 constexpr size_t ReferencedToThroughIndex = InverseBelongsToIndexOf<ThroughRecord, ReferencedRecord, ThroughSelector>;
3474
3475 // Filtering on the join record's foreign key is equivalent to joining the owning table back in and
3476 // filtering on its primary key - the caller already holds that primary key - and it keeps the owning
3477 // table out of the query, which matters when it is the same table as one already joined.
3478 return _connection.Query(RecordTableName<ReferencedRecord>)
3479 .Select()
3480 .Build([&](auto& query) {
3481 EnumerateRecordMembers<ReferencedRecord>([&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
3482 if constexpr (FieldWithStorage<ReferencedFieldType>)
3483 {
3484 query.Field(SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
3485 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
3486 }
3487 });
3488 })
3489 .InnerJoin(RecordTableName<ThroughRecord>,
3490 FieldNameAt<RecordPrimaryKeyIndex<ThroughRecord>, ThroughRecord>,
3491 FieldNameAt<ReferencedToThroughIndex, ReferencedRecord>)
3492 .Where(
3493 SqlQualifiedTableColumnName {
3494 RecordTableName<ThroughRecord>,
3495 FieldNameAt<ThroughToOwnerIndex, ThroughRecord>,
3496 },
3497 SqlWildcard);
3498}
3499
3500template <typename ReferencedRecord, typename ThroughSpec, typename Record, auto OwnerSelector, auto ThroughSelector>
3501void DataMapper::LoadHasOneThrough(Record& record,
3502 HasOneThrough<ReferencedRecord, ThroughSpec, OwnerSelector, ThroughSelector>& field)
3503{
3504 using ThroughRecord = ThroughRecordOf<ThroughSpec>;
3505
3506 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3507 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
3508
3509 ZoneScopedN("DataMapper::LoadHasOneThrough");
3510 ZoneTextObject(RecordTableName<ReferencedRecord>);
3511
3512 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
3513 auto query =
3514 BuildHasOneThroughSelectQuery<ReferencedRecord, ThroughRecord, Record, OwnerSelector, ThroughSelector>();
3515 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), primaryKeyField.Value()); link)
3516 field.EmplaceRecord(std::make_shared<ReferencedRecord>(std::move(*link)));
3517 });
3518}
3519
3520template <typename ReferencedRecord,
3521 typename ThroughRecord,
3522 typename Record,
3523 auto OwnerSelector,
3524 auto ThroughSelector,
3525 typename PKValue>
3526std::shared_ptr<ReferencedRecord> DataMapper::LoadHasOneThroughByPK(PKValue const& pkValue)
3527{
3528 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
3529
3530 auto query = BuildHasOneThroughSelectQuery<ReferencedRecord, ThroughRecord, Record, OwnerSelector, ThroughSelector>();
3531
3532 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), pkValue); link)
3533 return std::make_shared<ReferencedRecord>(std::move(*link));
3534
3535 return {};
3536}
3537
3538template <typename ReferencedRecord, typename ThroughRecord, typename Record, auto OwnerSelector, auto ReferencedSelector>
3539SqlSelectQueryBuilder DataMapper::BuildHasManyThroughSelectQuery()
3540{
3541 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3542 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
3543
3544 // The join record's foreign key pointing at the record owning this relationship.
3545 constexpr size_t ThroughToOwnerIndex = InverseBelongsToIndexOf<Record, ThroughRecord, OwnerSelector>;
3546
3547 // The join record's foreign key pointing at the referenced record.
3548 constexpr size_t ThroughToReferencedIndex = InverseBelongsToIndexOf<ReferencedRecord, ThroughRecord, ReferencedSelector>;
3549
3550 return _connection.Query(RecordTableName<ReferencedRecord>)
3551 .Select()
3552 .Build([&](auto& query) {
3553 EnumerateRecordMembers<ReferencedRecord>([&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
3554 if constexpr (FieldWithStorage<ReferencedFieldType>)
3555 {
3556 query.Field(SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
3557 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
3558 }
3559 });
3560 })
3561 .InnerJoin(RecordTableName<ThroughRecord>,
3562 FieldNameAt<ThroughToReferencedIndex, ThroughRecord>,
3563 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
3564 FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord> })
3565 .Where(
3566 SqlQualifiedTableColumnName {
3567 RecordTableName<ThroughRecord>,
3568 FieldNameAt<ThroughToOwnerIndex, ThroughRecord>,
3569 },
3570 SqlWildcard);
3571}
3572
3573template <typename ReferencedRecord,
3574 typename ThroughRecord,
3575 typename Record,
3576 auto OwnerSelector,
3577 auto ReferencedSelector,
3578 typename Callable>
3579void DataMapper::CallOnHasManyThrough(Record& record, Callable const& callback)
3580{
3581 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3582
3583 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
3584 auto query =
3585 BuildHasManyThroughSelectQuery<ReferencedRecord, ThroughRecord, Record, OwnerSelector, ReferencedSelector>();
3586 callback(query, primaryKeyField);
3587 });
3588}
3589
3590template <typename ReferencedRecord,
3591 typename ThroughRecord,
3592 typename Record,
3593 auto OwnerSelector,
3594 auto ReferencedSelector,
3595 typename PKValue,
3596 typename Callable>
3597void DataMapper::CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback)
3598{
3599 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3600
3601 auto query =
3602 BuildHasManyThroughSelectQuery<ReferencedRecord, ThroughRecord, Record, OwnerSelector, ReferencedSelector>();
3603 callback(query, pkValue);
3604}
3605
3606template <typename ReferencedRecord, typename ThroughSpec, typename Record, auto OwnerSelector, auto ReferencedSelector>
3607void DataMapper::LoadHasManyThrough(Record& record,
3608 HasManyThrough<ReferencedRecord, ThroughSpec, OwnerSelector, ReferencedSelector>& field)
3609{
3610 using ThroughRecord = ThroughRecordOf<ThroughSpec>;
3611
3612 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3613
3614 ZoneScopedN("DataMapper::LoadHasManyThrough");
3615 ZoneTextObject(RecordTableName<ReferencedRecord>);
3616
3617 CallOnHasManyThrough<ReferencedRecord, ThroughRecord, Record, OwnerSelector, ReferencedSelector>(
3618 record, [&](SqlSelectQueryBuilder& selectQuery, auto& primaryKeyField) {
3619 field.Emplace(detail::ToSharedPtrList(Query<ReferencedRecord>(selectQuery.All(), primaryKeyField.Value())));
3620 });
3621}
3622
3623template <typename Record>
3624void DataMapper::LoadRelations(Record& record)
3625{
3626 static_assert(!std::is_const_v<Record>);
3627 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3628
3629 ZoneScopedN("DataMapper::LoadRelations");
3630 ZoneTextObject(RecordTableName<Record>);
3631
3632#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
3633 constexpr auto ctx = std::meta::access_context::current();
3634 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
3635 {
3636 using FieldType = typename[:std::meta::type_of(el):];
3637 if constexpr (IsBelongsTo<FieldType>)
3638 {
3639 auto& field = record.[:el:];
3640 field.AdoptFetchedRecord(LoadBelongsTo<FieldType>(field.Value()));
3641 }
3642 else if constexpr (IsCompositeForeignKey<FieldType>)
3643 {
3644 LoadCompositeForeignKey(record, record.[:el:]);
3645 }
3646 else if constexpr (IsHasMany<FieldType>)
3647 {
3648 LoadHasMany(record, record.[:el:]);
3649 }
3650 else if constexpr (IsHasOneThrough<FieldType>)
3651 {
3652 LoadHasOneThrough(record, record.[:el:]);
3653 }
3654 else if constexpr (IsHasManyThrough<FieldType>)
3655 {
3656 LoadHasManyThrough(record, record.[:el:]);
3657 }
3658 }
3659#else
3660 EnumerateRecordMembers(record, [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
3661 if constexpr (IsBelongsTo<FieldType>)
3662 {
3663 field.AdoptFetchedRecord(LoadBelongsTo<FieldType>(field.Value()));
3664 }
3665 else if constexpr (IsCompositeForeignKey<FieldType>)
3666 {
3667 LoadCompositeForeignKey(record, field);
3668 }
3669 else if constexpr (IsHasMany<FieldType>)
3670 {
3671 LoadHasMany(record, field);
3672 }
3673 else if constexpr (IsHasOneThrough<FieldType>)
3674 {
3675 LoadHasOneThrough(record, field);
3676 }
3677 else if constexpr (IsHasManyThrough<FieldType>)
3678 {
3679 LoadHasManyThrough(record, field);
3680 }
3681 });
3682#endif
3683}
3684
3685/// Sets the primary key field(s) of the given record to the specified id value.
3686template <typename Record, typename ValueType>
3687inline LIGHTWEIGHT_FORCE_INLINE void DataMapper::SetId(Record& record, ValueType&& id)
3688{
3689 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3690 // static_assert(HasPrimaryKey<Record>);
3691
3692#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
3693
3694 auto constexpr ctx = std::meta::access_context::current();
3695 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
3696 {
3697 using FieldType = typename[:std::meta::type_of(el):];
3698 if constexpr (IsField<FieldType>)
3699 {
3700 // Only a generated key member receives the value; a PrimaryKey::Manual member is the caller's.
3701 if constexpr (FieldType::IsAutoAssignPrimaryKey || FieldType::IsAutoIncrementPrimaryKey)
3702 {
3703 record.[:el:] = std::forward<ValueType>(id);
3704 }
3705 }
3706 }
3707#else
3708 EnumerateRecordMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
3709 if constexpr (IsField<FieldType>)
3710 {
3711 // Only a generated key member receives the value; a PrimaryKey::Manual member is the caller's.
3712 if constexpr (FieldType::IsAutoAssignPrimaryKey || FieldType::IsAutoIncrementPrimaryKey)
3713 {
3714 field = std::forward<FieldType>(id);
3715 }
3716 }
3717 });
3718#endif
3719}
3720
3721/// Binds all output columns of the record via the given cursor.
3722template <typename Record, size_t InitialOffset>
3723inline LIGHTWEIGHT_FORCE_INLINE Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
3724{
3725 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3726 return BindOutputColumns<std::make_integer_sequence<size_t, RecordMemberCount<Record>>, Record, InitialOffset>(record,
3727 cursor);
3728}
3729
3730template <typename ElementMask, typename Record, size_t InitialOffset>
3731Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
3732{
3733 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3734 static_assert(!std::is_const_v<Record>);
3735
3736#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
3737 auto constexpr ctx = std::meta::access_context::current();
3738 SQLSMALLINT i = SQLSMALLINT { InitialOffset };
3739 template for (constexpr auto index: define_static_array(template_arguments_of(^^ElementMask)) | std::views::drop(1))
3740 {
3741 constexpr auto el = nonstatic_data_members_of(^^Record, ctx)[[:index:]];
3742 using FieldType = typename[:std::meta::type_of(el):];
3743 if constexpr (IsField<FieldType>)
3744 {
3745 cursor.BindOutputColumn(i++, &record.[:el:].MutableValue());
3746 }
3747 else if constexpr (SqlOutputColumnBinder<FieldType>)
3748 {
3749 cursor.BindOutputColumn(i++, &record.[:el:]);
3750 }
3751 }
3752#else
3753 EnumerateRecordMembers<ElementMask>(
3754 record, [&cursor, i = SQLUSMALLINT { InitialOffset }]<size_t I, typename Field>(Field& field) mutable {
3755 if constexpr (IsField<Field>)
3756 {
3757 cursor.BindOutputColumn(i++, &field.MutableValue());
3758 }
3759 else if constexpr (SqlOutputColumnBinder<Field>)
3760 {
3761 cursor.BindOutputColumn(i++, &field);
3762 }
3763 });
3764#endif
3765
3766 return record;
3767}
3768template <typename Load>
3769auto DataMapper::RunRelationLoad(detail::RelationLoadSource& source, Load const& load)
3770 -> RelationResult<std::invoke_result_t<Load const&, DataMapper&>>
3771{
3772 using Result = std::invoke_result_t<Load const&, DataMapper&>;
3773 return source.Borrow().and_then([&load](std::shared_ptr<DataMapper> const& borrowed) -> RelationResult<Result> {
3774 try
3775 {
3776 if constexpr (std::is_void_v<Result>)
3777 {
3778 load(*borrowed);
3779 return {};
3780 }
3781 else
3782 return load(*borrowed);
3783 }
3784 catch (std::exception const& error)
3785 {
3786 SqlLogger::GetLogger().OnWarning(std::format("Loading a relation failed: {}", error.what()));
3787 return std::unexpected { RelationError::QueryFailed };
3788 }
3789 });
3790}
3791
3792template <typename Each, typename Stream>
3793RelationResult<void> DataMapper::RunRelationStream(detail::RelationLoadSource& source,
3794 Each const& each,
3795 Stream const& stream)
3796{
3797 auto callbackError = std::exception_ptr {};
3798 auto const guarded = [&each, &callbackError](auto const& row) {
3799 try
3800 {
3801 each(row);
3802 }
3803 catch (...)
3804 {
3805 callbackError = std::current_exception();
3806 throw;
3807 }
3808 };
3809 auto result = RunRelationLoad(source, [&](DataMapper& dm) { stream(dm, guarded); });
3810 if (callbackError)
3811 std::rethrow_exception(callbackError);
3812 return result;
3813}
3814
3815template <typename Record, typename QueryText, typename Callable, typename... InputParameters>
3816void DataMapper::StreamRecords(QueryText const& query, Callable const& each, InputParameters const&... inputParameters)
3817{
3818 _stmt.Prepare(query);
3819 auto cursor = _stmt.Execute(inputParameters...);
3820
3821 auto record = Record {};
3822 BindOutputColumns(record, cursor);
3823 while (cursor.FetchRow())
3824 {
3825 // Installed only now that the row is fetched: the loaders capture the record's key values when
3826 // installed, and before the fetch those are still default-constructed.
3828 each(record);
3829
3830 // Reset before rebinding for the next row. The same record instance is reused across rows, and a
3831 // fetch does not necessarily overwrite the whole of a variable-width buffer: a shorter value
3832 // leaves the tail of the previous one in place, so a string column can come back as a blend of
3833 // two rows. Assigning a fresh record clears every field's buffer and indicator first.
3834 record = Record {};
3835 BindOutputColumns(record, cursor);
3836 }
3837}
3838
3839template <typename Record>
3840// NOLINTNEXTLINE(readability-function-cognitive-complexity)
3842{
3843 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
3844
3845 // Every loader below borrows a mapper of its own from this source for the duration of one load,
3846 // rather than running on this mapper (which a record may outlive) or on a shared thread-local one:
3847 // no two loads - nor a load nested inside a streaming Each() - share a statement or connection.
3848 auto const& source = RelationLoadSourceForLoaders();
3849
3850 auto const callback = [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
3851 if constexpr (IsBelongsTo<FieldType>)
3852 {
3853 using ReferencedRecord = FieldType::ReferencedRecord;
3854 field.SetAutoLoader(typename FieldType::Loader {
3855 .loadReference = [source, value = field.Value()]() -> RelationResult<ReferencedRecord> {
3856 return RunRelationLoad(*source, [&](DataMapper& dm) { return dm.LoadBelongsTo<FieldType>(value); })
3857 .and_then([](std::optional<ReferencedRecord> loaded) -> RelationResult<ReferencedRecord> {
3858 if (!loaded)
3859 return std::unexpected { RelationError::NotFound };
3860 return std::move(*loaded);
3861 });
3862 },
3863 });
3864 }
3865 if constexpr (IsCompositeForeignKey<FieldType>)
3866 {
3867 using ReferencedRecord = FieldType::ReferencedRecord;
3868
3869 // Captured by value, evaluated now while `record` is known to be live - not a pointer to
3870 // `record` read later from inside the closure. A `std::optional<Record>` returned by value
3871 // from a query method (QuerySingle, First, ...) is not guaranteed to keep its address: NRVO
3872 // is not mandated by the standard, and - verified - does not reliably apply to the fuller
3873 // body of those functions in at least one real build configuration, so a captured pointer
3874 // can end up pointing at stack memory already reused for something else by the time the
3875 // loader runs. The trade-off is the same one HasMany/BelongsTo already make: repointing the
3876 // foreign key after this point does not change what the relation resolves to.
3877 //
3878 // OrderedValuesOf() - not ValuesOf() - because QuerySingle emits one WHERE predicate per
3879 // primary key member in the *referenced record's* member declaration order and binds its
3880 // arguments positionally. Passing them in connection-declaration order would bind each
3881 // value to the wrong predicate whenever the two orders differ, which with same-typed key
3882 // columns fetches a wrong row rather than failing. See CompositeKeyOrderingTests.cpp.
3883 field.SetAutoLoader(typename FieldType::Loader {
3884 .loadReference = [source, keys = FieldType::OrderedValuesOf(record)]()
3885 -> RelationResult<std::shared_ptr<ReferencedRecord>> {
3886 return RunRelationLoad(*source,
3887 [&](DataMapper& dm) { return dm.LoadCompositeForeignKeyRecord<FieldType>(keys); })
3888 .and_then(NotFoundIfNull<ReferencedRecord>);
3889 },
3890 });
3891 }
3892 if constexpr (IsHasMany<FieldType>)
3893 {
3894 if constexpr (HasPrimaryKey<Record>)
3895 {
3896 using ReferencedRecord = FieldType::ReferencedRecord;
3898 // Capture the PK value by value to avoid dangling references if the record is moved.
3899 auto pkValue = GetPrimaryKeyField(record);
3900 hasMany.SetAutoLoader(typename FieldType::Loader {
3901 .count = [source, pkValue]() -> RelationResult<size_t> {
3902 return RunRelationLoad(*source, [&](DataMapper& dm) {
3903 auto selectQuery =
3904 dm.BuildHasManySelectQuery<Record, ReferencedRecord, FieldType::InverseSelector>();
3905 dm._stmt.Prepare(selectQuery.Count());
3906 SqlResultCursor cursor = dm._stmt.Execute(pkValue);
3907 size_t count = 0;
3908 if (cursor.FetchRow())
3909 count = cursor.GetColumn<size_t>(1);
3910 return count;
3911 });
3912 },
3913 .all = [source, pkValue]() -> RelationResult<typename FieldType::ReferencedRecordList> {
3914 return RunRelationLoad(*source, [&](DataMapper& dm) {
3915 auto selectQuery =
3916 dm.BuildHasManySelectQuery<Record, ReferencedRecord, FieldType::InverseSelector>();
3917 return detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pkValue));
3918 });
3919 },
3920 .each = [source, pkValue](auto const& each) -> RelationResult<void> {
3921 return RunRelationStream(*source, each, [&](DataMapper& dm, auto const& guarded) {
3922 auto selectQuery =
3923 dm.BuildHasManySelectQuery<Record, ReferencedRecord, FieldType::InverseSelector>();
3924 dm.StreamRecords<ReferencedRecord>(selectQuery.All(), guarded, pkValue);
3925 });
3926 },
3927 });
3928 }
3929 }
3930 if constexpr (IsHasOneThrough<FieldType> && HasPrimaryKey<Record>)
3931 {
3932 using ReferencedRecord = FieldType::ReferencedRecord;
3933 using ThroughRecord = FieldType::ThroughRecord;
3934 // Capture the PK value by value to avoid dangling references if the record is moved.
3935 auto pkValue = GetPrimaryKeyField(record);
3936 field.SetAutoLoader(typename FieldType::Loader {
3937 .loadReference = [source, pkValue]() -> RelationResult<std::shared_ptr<ReferencedRecord>> {
3938 return RunRelationLoad(*source,
3939 [&](DataMapper& dm) {
3940 return dm.LoadHasOneThroughByPK<ReferencedRecord,
3941 ThroughRecord,
3942 Record,
3943 FieldType::OwnerSelector,
3944 FieldType::ThroughSelector>(pkValue);
3945 })
3946 .and_then(NotFoundIfNull<ReferencedRecord>);
3947 },
3948 });
3949 }
3950 if constexpr (IsHasManyThrough<FieldType> && HasPrimaryKey<Record>)
3951 {
3952 using ReferencedRecord = FieldType::ReferencedRecord;
3953 using ThroughRecord = FieldType::ThroughRecord;
3954 // Capture the PK value by value to avoid dangling references if the record is moved.
3955 auto pkValue = GetPrimaryKeyField(record);
3956 field.SetAutoLoader(typename FieldType::Loader {
3957 .count = [source, pkValue]() -> RelationResult<size_t> {
3958 return RunRelationLoad(*source, [&](DataMapper& dm) {
3959 size_t count = 0;
3960 dm.CallOnHasManyThroughByPK<ReferencedRecord,
3961 ThroughRecord,
3962 Record,
3963 FieldType::OwnerSelector,
3964 FieldType::ReferencedSelector>(
3965 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
3966 dm._stmt.Prepare(selectQuery.Count());
3967 SqlResultCursor cursor = dm._stmt.Execute(pk);
3968 if (cursor.FetchRow())
3969 count = cursor.GetColumn<size_t>(1);
3970 });
3971 return count;
3972 });
3973 },
3974 .all = [source, pkValue]() -> RelationResult<typename FieldType::ReferencedRecordList> {
3975 return RunRelationLoad(*source, [&](DataMapper& dm) {
3976 typename FieldType::ReferencedRecordList result;
3977 dm.CallOnHasManyThroughByPK<ReferencedRecord,
3978 ThroughRecord,
3979 Record,
3980 FieldType::OwnerSelector,
3981 FieldType::ReferencedSelector>(
3982 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
3983 result = detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pk));
3984 });
3985 return result;
3986 });
3987 },
3988 .each = [source, pkValue](auto const& each) -> RelationResult<void> {
3989 return RunRelationStream(*source, each, [&](DataMapper& dm, auto const& guarded) {
3990 dm.CallOnHasManyThroughByPK<ReferencedRecord,
3991 ThroughRecord,
3992 Record,
3993 FieldType::OwnerSelector,
3994 FieldType::ReferencedSelector>(
3995 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
3996 dm.StreamRecords<ReferencedRecord>(selectQuery.All(), guarded, pk);
3997 });
3998 });
3999 },
4000 });
4001 }
4002 };
4003
4004#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
4005 constexpr auto ctx = std::meta::access_context::current();
4006
4007 Reflection::template_for<0, nonstatic_data_members_of(^^Record, ctx).size()>([&callback, &record]<auto I>() {
4008 constexpr auto localctx = std::meta::access_context::current();
4009 constexpr auto members = define_static_array(nonstatic_data_members_of(^^Record, localctx));
4010 using FieldType = typename[:std::meta::type_of(members[I]):];
4011 callback.template operator()<I, FieldType>(record.[:members[I]:]);
4012 });
4013#else
4014 EnumerateRecordMembers(record, callback);
4015#endif
4016}
4017
4018template <typename T>
4019std::optional<T> DataMapper::Execute(std::string_view sqlQueryString)
4020{
4021 ZoneScopedN("DataMapper::Execute(string)");
4022 ZoneTextObject(sqlQueryString);
4023 return _stmt.ExecuteDirectScalar<T>(sqlQueryString);
4024}
4025
4026} // namespace Lightweight
4027
4028#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.
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.
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.
std::function< SqlDateTime()> TimestampClock
A source of the current time, used to fill fields marked with a FieldTimestamp.
SqlAllFieldsQueryBuilder< Record, QueryOptions > Query()
std::optional< Record > QuerySingle(PrimaryKeyTypes &&... primaryKeys)
Queries a single record (based on primary key) from the database.
void SetTimestampClock(TimestampClock clock) noexcept
Sets the clock that fills the auto-maintained timestamp fields (see FieldTimestamp).
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.
SqlDateTime CurrentTimestamp() const
Reads the clock that fills the auto-maintained timestamp fields.
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()
void Update(Record &record)
SqlQueryBuilder FromTable(std::string_view tableName)
Constructs an SQL query builder for the given table name.
ModifiedState
Enum to set the modified state of a record.
RecordPrimaryKeyType< Record > Create(Record &record)
Creates a new record in the database.
Async::Task< std::size_t > DeleteAsync(Record const &record)
Asynchronously deletes record.
std::vector< Record > Query(SqlSelectQueryBuilder::ComposedQuery const &selectQuery, InputParameters &&... inputParameters)
This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
Definition HasMany.hpp:67
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
Definition HasMany.hpp:228
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...
auto Delete()
Executes a DELETE query.
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 SqlDeleteQueryBuilder Delete(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
LIGHTWEIGHT_API SqlInsertQueryBuilder Insert(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
LIGHTWEIGHT_API SqlSelectQueryStarter Select(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
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.
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:94
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)
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:52
Requires that T maps onto a column of its record's table.
Definition Record.hpp:419
@ Execute
SQLExecute of a prepared statement.
LIGHTWEIGHT_FORCE_INLINE RecordPrimaryKeyType< Record > GetPrimaryKeyField(Record const &record) noexcept
Definition Record.hpp:615
constexpr decltype(auto) GetRecordMemberAt(Record &&record)
Returns a reference to the member at index I — from the descriptor if present, else via reflection.
std::expected< T, RelationError > RelationResult
The outcome of a relation access: the requested value, or why it is unavailable.
Definition Error.hpp:44
constexpr std::string_view FieldNameAt
Returns the SQL field name of the given field index in the record.
Definition Utils.hpp:285
constexpr void EnumerateRecordMembers(Record &record, Callable &&callable)
Invokes callable as callable<I>(member) for each member of record.
detail::RecordMemberTypeOfDispatch< I, std::remove_cvref_t< Record >, HasDescription< Record > >::type RecordMemberTypeOf
Type of the member at index I — from the descriptor if present, else via reflection.
@ UpdatedAt
The field holds the time the record was last written.
@ None
The field is not maintained by the data mapper.
@ NotFound
There is nothing to load: the foreign key is NULL, or the referenced row does not exist.
T ValueType
The underlying value type of this field.
Definition Field.hpp:156
static constexpr auto IsOptional
Indicates if the field is optional, i.e., it can be NULL.
Definition Field.hpp:203
static SqlGuid Create() noexcept
Creates a new non-empty GUID.
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Utils.hpp:342
Represents a value that can be any of the supported SQL data types.