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