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