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