Lightweight 0.20260625.0
Loading...
Searching...
No Matches
SqlStatement.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#if defined(_WIN32) || defined(_WIN64)
6 #include <Windows.h>
7#endif
8
9#include "Api.hpp"
10#include "DataBinder/Core.hpp"
11#include "DataBinder/SqlDate.hpp"
12#include "DataBinder/SqlDateTime.hpp"
13#include "DataBinder/SqlFixedString.hpp"
14#include "DataBinder/SqlGuid.hpp"
15#include "DataBinder/SqlNumeric.hpp"
16#include "DataBinder/StringInterface.hpp"
17#include "DataBinder/UnicodeConverter.hpp"
18#include "DataMapper/Record.hpp"
19#include "SqlConnection.hpp"
20#include "SqlQuery.hpp"
21#include "SqlQueryFormatter.hpp"
22#include "SqlServerType.hpp"
23#include "TracyProfiler.hpp"
24#include "Utils.hpp"
25
26#include <algorithm>
27#include <array>
28#include <cstdint>
29#include <cstring>
30#include <expected>
31#include <functional>
32#include <optional>
33#include <ranges>
34#include <source_location>
35#include <span>
36#include <stdexcept>
37#include <type_traits>
38#include <vector>
39
40#include <sql.h>
41#include <sqlext.h>
42#include <sqlspi.h>
43#include <sqltypes.h>
44
45namespace Lightweight
46{
47
48struct SqlRawColumn;
49
50/// @brief Represents an SQL query object, that provides a ToSql() method.
51template <typename QueryObject>
52concept SqlQueryObject = requires(QueryObject const& queryObject) {
53 { queryObject.ToSql() } -> std::convertible_to<std::string>;
54};
55
56class SqlResultCursor;
57class SqlVariantRowCursor;
58class RowArrayCursor;
59
60/// @brief High level API for (prepared) raw SQL statements
61///
62/// SQL prepared statement lifecycle:
63/// 1. Prepare the statement
64/// 2. Optionally bind output columns to local variables
65/// 3. Execute the statement (optionally with input parameters)
66/// 4. Fetch rows (if any)
67/// 5. Repeat steps 3 and 4 as needed
68class [[nodiscard]] SqlStatement final: public SqlDataBinderCallback
69{
70 public:
71 /// Construct a new SqlStatement object, using a new connection, and connect to the default database.
72 LIGHTWEIGHT_API SqlStatement();
73
74 /// Move constructor.
75 LIGHTWEIGHT_API SqlStatement(SqlStatement&& other) noexcept;
76 /// Move assignment operator.
77 LIGHTWEIGHT_API SqlStatement& operator=(SqlStatement&& other) noexcept;
78
79 SqlStatement(SqlStatement const&) noexcept = delete;
80 SqlStatement& operator=(SqlStatement const&) noexcept = delete;
81
82 /// Construct a new SqlStatement object, using the given connection.
83 LIGHTWEIGHT_API explicit SqlStatement(SqlConnection& relatedConnection);
84
85 /// Construct a new empty SqlStatement object. No SqlConnection is associated with this statement.
86 LIGHTWEIGHT_API explicit SqlStatement(std::nullopt_t /*nullopt*/);
87
88 LIGHTWEIGHT_API ~SqlStatement() noexcept final;
89
90 /// Checks whether the statement's connection is alive and the statement handle is valid.
91 [[nodiscard]] LIGHTWEIGHT_API bool IsAlive() const noexcept;
92
93 /// Checks whether the statement has been prepared.
94 [[nodiscard]] LIGHTWEIGHT_API bool IsPrepared() const noexcept;
95
96 /// Retrieves the connection associated with this statement.
97 [[nodiscard]] LIGHTWEIGHT_API SqlConnection& Connection() noexcept;
98
99 /// Retrieves the connection associated with this statement.
100 [[nodiscard]] LIGHTWEIGHT_API SqlConnection const& Connection() const noexcept;
101
102 /// Retrieves the last error information with respect to this SQL statement handle.
103 [[nodiscard]] LIGHTWEIGHT_API SqlErrorInfo LastError() const;
104
105 /// Creates a new query builder for the given table, compatible with the SQL server being connected.
106 LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const& table = {}) const;
107
108 /// Creates a new query builder for the given table with an alias, compatible with the SQL server being connected.
109 [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const& table,
110 std::string_view const& tableAlias) const;
111
112 /// Retrieves the native handle of the statement.
113 [[nodiscard]] LIGHTWEIGHT_API SQLHSTMT NativeHandle() const noexcept;
114
115 /// Prepares the statement for execution.
116 ///
117 /// @note When preparing a new SQL statement the previously executed statement, yielding a result set,
118 /// must have been closed.
119 LIGHTWEIGHT_API void Prepare(std::string_view query) &;
120
121 /// Prepares the statement for execution on an rvalue reference and returns the statement.
122 LIGHTWEIGHT_API SqlStatement Prepare(std::string_view query) &&;
123
124 /// Prepares the statement for execution.
125 ///
126 /// @note When preparing a new SQL statement the previously executed statement, yielding a result set,
127 /// must have been closed.
128 void Prepare(SqlQueryObject auto const& queryObject) &;
129
130 /// Prepares the statement from a query object on an rvalue reference and returns the statement.
131 SqlStatement Prepare(SqlQueryObject auto const& queryObject) &&;
132
133 /// Retrieves the last prepared query string.
134 [[nodiscard]] std::string const& PreparedQuery() const noexcept;
135
136 /// Binds an input parameter to the prepared statement at the given column index.
137 template <SqlInputParameterBinder Arg>
138 void BindInputParameter(SQLSMALLINT columnIndex, Arg const& arg);
139
140 /// Binds an input parameter to the prepared statement at the given column index with a column name hint.
141 template <SqlInputParameterBinder Arg, typename ColumnName>
142 void BindInputParameter(SQLSMALLINT columnIndex, Arg const& arg, ColumnName&& columnNameHint);
143
144 /// Binds the given arguments to the prepared statement and executes it.
145 template <SqlInputParameterBinder... Args>
146 [[nodiscard]] SqlResultCursor Execute(Args const&... args);
147
148 /// Binds the given arguments to the prepared statement and executes it.
149 [[nodiscard]] LIGHTWEIGHT_API SqlResultCursor ExecuteWithVariants(std::vector<SqlVariant> const& args);
150
151 /// Executes the prepared statement on a batch of data.
152 ///
153 /// Each parameter represents a column, to be bound as input parameter.
154 /// The element types of each column container must be explicitly supported.
155 ///
156 /// In order to support column value types, their underlying storage must be contiguous.
157 /// Also the input range itself must be contiguous.
158 /// If any of these conditions are not met, the function will not compile - use ExecuteBatch() instead.
159 template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::contiguous_range... MoreColumnBatches>
160 [[nodiscard]] SqlResultCursor ExecuteBatchNative(FirstColumnBatch const& firstColumnBatch,
161 MoreColumnBatches const&... moreColumnBatches);
162
163 /// Executes the prepared statement on a batch of data.
164 ///
165 /// Each parameter represents a column, to be bound as input parameter,
166 /// and the number of elements in these bound column containers will
167 /// mandate how many executions will happen.
168 ///
169 /// This function will bind and execute each row separately,
170 /// which is less efficient than ExecuteBatchNative(), but works non-contiguous input ranges.
171 template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::range... MoreColumnBatches>
172 [[nodiscard]] SqlResultCursor ExecuteBatchSoft(FirstColumnBatch const& firstColumnBatch,
173 MoreColumnBatches const&... moreColumnBatches);
174
175 /// Executes the prepared statement on a batch of data.
176 ///
177 /// Each parameter represents a column, to be bound as input parameter,
178 /// and the number of elements in these bound column containers will
179 /// mandate how many executions will happen.
180 template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::range... MoreColumnBatches>
181 [[nodiscard]] SqlResultCursor ExecuteBatch(FirstColumnBatch const& firstColumnBatch,
182 MoreColumnBatches const&... moreColumnBatches);
183
184 /// Executes the prepared statement on a batch of SqlRawColumn-prepared data.
185 ///
186 /// @param columns The columns to bind as input parameters.
187 /// @param rowCount The number of rows to execute.
188 [[nodiscard]] LIGHTWEIGHT_API SqlResultCursor ExecuteBatch(std::span<SqlRawColumn const> columns, size_t rowCount);
189
190 /// Executes the prepared statement once per row of a *row-major* batch, preferring native ODBC
191 /// row-wise array binding (a single zero-copy @c SQLExecute) and transparently falling back to a
192 /// prepare-once + per-row execute when native binding is not possible.
193 ///
194 /// Unlike the column-major @c ExecuteBatch overloads, the data here is laid out as an array of row
195 /// structs (e.g. records). Each @p accessors invocable maps a row to one bound column's value,
196 /// returning a reference into the row (so the native path binds the value in place):
197 /// @code
198 /// stmt.ExecuteBatch(std::span { records }, [](Record const& r) -> auto const& { return r.id.Value(); }, ...);
199 /// @endcode
200 ///
201 /// The native row-wise path is taken when every column value type is row-bindable
202 /// (@c SqlNativeRowBindableValue, or @c std::optional of such a non-numeric type), every accessor
203 /// returns an lvalue reference, the row stride satisfies the indicator-alignment requirement, and the
204 /// driver advertises parameter-array support (@ref SqlConnection::SupportsNativeRowBatch). A
205 /// per-row runtime stride check guards against accessors that are not constant-offset subobjects.
206 /// Otherwise the soft path is used, which correctly binds every supported type (strings, binary,
207 /// variant, @c std::optional of any type, …) one row at a time.
208 ///
209 /// @param rows Contiguous range of row structs (e.g. @c std::span<Record const>).
210 /// @param accessors One invocable per bound column; @c accessor(row) yields that column's value.
211 /// @return A result cursor for the executed batch (empty when @p rows is empty).
212 template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
213 requires(sizeof...(ColumnAccessors) >= 1
214 && (std::invocable<ColumnAccessors const&, std::ranges::range_value_t<Rows> const&> && ...))
215 [[nodiscard]] SqlResultCursor ExecuteBatch(Rows const& rows, ColumnAccessors const&... accessors);
216
217 /// Executes the given query directly.
218 [[nodiscard]] LIGHTWEIGHT_API SqlResultCursor
219 ExecuteDirect(std::string_view const& query, std::source_location location = std::source_location::current());
220
221 /// Executes the given query directly.
222 [[nodiscard]] SqlResultCursor ExecuteDirect(SqlQueryObject auto const& query,
223 std::source_location location = std::source_location::current());
224
225 /// Executes @p query and prepares bulk row-array fetching with up to @p arrayDepth rows per
226 /// SQLFetchScroll round-trip.
227 ///
228 /// This is a fast-path alternative to the per-cell SQLGetData loop used by the regular result
229 /// cursor: it binds one contiguous buffer per result column and materializes whole row blocks
230 /// per ODBC round-trip. Only fixed-stride column types are supported (integers, floating point,
231 /// and bounded character columns). LOB / unbounded columns (varchar(max)/text/varbinary(max))
232 /// are rejected by the returned cursor's construction.
233 ///
234 /// @param query The SQL query to execute.
235 /// @param arrayDepth Maximum number of rows materialized per SQLFetchScroll call (must be > 0).
236 /// @return A RowArrayCursor bound to this statement's result set.
237 [[nodiscard]] LIGHTWEIGHT_API RowArrayCursor ExecuteBatchFetch(std::string_view query, std::size_t arrayDepth);
238
239 /// Executes an SQL migration query, as created b the callback.
240 template <typename Callable>
241 requires std::invocable<Callable, SqlMigrationQueryBuilder&>
242 void MigrateDirect(Callable const& callable, std::source_location location = std::source_location::current());
243
244 /// Executes the given query, assuming that only one result row and column is affected, that one will be
245 /// returned.
246 template <typename T>
247 requires(!std::same_as<T, SqlVariant>)
248 [[nodiscard]] std::optional<T> ExecuteDirectScalar(std::string_view const& query,
249 std::source_location location = std::source_location::current());
250
251 /// Executes the given query and returns the single result as an SqlVariant.
252 template <typename T>
253 requires(std::same_as<T, SqlVariant>)
254 [[nodiscard]] T ExecuteDirectScalar(std::string_view const& query,
255 std::source_location location = std::source_location::current());
256
257 /// Executes the given query, assuming that only one result row and column is affected, that one will be
258 /// returned.
259 template <typename T>
260 requires(!std::same_as<T, SqlVariant>)
261 [[nodiscard]] std::optional<T> ExecuteDirectScalar(SqlQueryObject auto const& query,
262 std::source_location location = std::source_location::current());
263
264 /// Executes the given query object and returns the single result as an SqlVariant.
265 template <typename T>
266 requires(std::same_as<T, SqlVariant>)
267 [[nodiscard]] T ExecuteDirectScalar(SqlQueryObject auto const& query,
268 std::source_location location = std::source_location::current());
269
270 /// Retrieves the last insert ID of the given table.
271 [[nodiscard]] LIGHTWEIGHT_API size_t LastInsertId(std::string_view tableName);
272
273 private:
274 friend class SqlResultCursor;
275 friend class RowArrayCursor;
276
277 [[nodiscard]] LIGHTWEIGHT_API size_t NumRowsAffected() const;
278 [[nodiscard]] LIGHTWEIGHT_API size_t NumColumnsAffected() const;
279 [[nodiscard]] LIGHTWEIGHT_API bool FetchRow();
280 [[nodiscard]] LIGHTWEIGHT_API std::expected<bool, SqlErrorInfo> TryFetchRow(
281 std::source_location location = std::source_location::current()) noexcept;
282 void CloseCursor() noexcept;
283
284 /// @brief Binds the given output column variables to the result columns of this statement.
285 /// @tparam Args ODBC-bindable output column types.
286 /// @param args Pointers to caller-owned storage for each result column, in order.
287 template <SqlOutputColumnBinder... Args>
288 void BindOutputColumns(Args*... args);
289
290 /// @brief Binds the members of @p records to the result columns of this statement
291 /// in declaration order, via reflection.
292 /// @tparam Records Aggregate record types whose members map to result columns.
293 /// @param records Pointers to caller-owned record instances.
294 template <typename... Records>
295 requires(((std::is_class_v<Records> && std::is_aggregate_v<Records>) && ...))
296 void BindOutputColumnsToRecord(Records*... records);
297
298 /// @brief Binds a single output column variable to the result column at @p columnIndex.
299 /// @tparam T An ODBC-bindable output column type.
300 /// @param columnIndex 1-based result column index.
301 /// @param arg Pointer to caller-owned storage for the column value.
302 template <SqlOutputColumnBinder T>
303 void BindOutputColumn(SQLUSMALLINT columnIndex, T* arg);
304
305 template <SqlGetColumnNativeType T>
306 [[nodiscard]] bool GetColumn(SQLUSMALLINT column, T* result) const;
307
308 template <SqlGetColumnNativeType T>
309 [[nodiscard]] T GetColumn(SQLUSMALLINT column) const;
310
311 /// @brief Native row-wise batch execution: binds each column in place over @p rows and submits the
312 /// whole batch in a single @c SQLExecute. Precondition: every column is row-bindable.
313 template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
314 [[nodiscard]] SqlResultCursor ExecuteBatchNativeRowWise(Rows const& rows, ColumnAccessors const&... accessors);
315
316 /// @brief Soft row-major batch execution: binds and executes each row individually. Works for every
317 /// supported column type and is the fallback when native row-wise binding does not apply.
318 template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
319 [[nodiscard]] SqlResultCursor ExecuteBatchSoftRowMajor(Rows const& rows, ColumnAccessors const&... accessors);
320
321 /// @brief Native row-wise array fetch: materializes the already-executed result set into @p out by
322 /// binding every result column row-wise over a contiguous block of @p out's records and pulling whole
323 /// blocks per @c SQLFetchScroll round-trip. The read-side mirror of @c ExecuteBatchNativeRowWise.
324 ///
325 /// Each @p accessors invocable maps a record to one bound column's mutable value reference (the same
326 /// declaration-order column set the per-row path binds), so the driver writes results in place — no
327 /// per-cell @c SQLGetData and no intermediate copy. @p out is grown a block at a time and trimmed to
328 /// the exact row count on the final partial block.
329 ///
330 /// @pre Every accessor's value type satisfies @c SqlRowWiseFetchableColumn and
331 /// @c sizeof(Record) % alignof(SQLLEN) == 0 (so the row-strided indicator slots stay aligned).
332 /// The caller (DataMapper) guarantees both before selecting this path.
333 /// @param out Destination vector; results are appended to its current contents.
334 /// @param arrayDepth Requested maximum rows per @c SQLFetchScroll (clamped to a memory budget).
335 /// @param accessors One invocable per result column; @c accessor(record) yields its mutable value.
336 template <typename Record, typename... ColumnAccessors>
337 void FetchAllRowWise(std::vector<Record>& out, std::size_t arrayDepth, ColumnAccessors const&... accessors);
338
339 /// @brief Row-wise array-binds one output column over a record block; returns the row-strided
340 /// indicator buffer to feed @c FinalizeRowWiseOutputColumn. For optional columns every row's
341 /// optional is pre-engaged so the contained storage is valid to bind into.
342 template <typename ValueType>
343 [[nodiscard]] SQLLEN* BindRowWiseOutputColumn(SQLUSMALLINT column,
344 void* base0,
345 std::size_t rowStride,
346 std::size_t depth);
347
348 /// @brief Issues the row-wise @c SQLBindCol for one non-optional value type @p Value at @p base0 (the
349 /// value slot in record 0; the driver strides it by the active @c SQL_ATTR_ROW_BIND_TYPE). Fixed-
350 /// capacity char strings bind their inline buffer as @c SQL_C_CHAR (length fixed up per row
351 /// afterwards); all other types bind in place via their @c SqlDataBinder::OutputColumn.
352 template <typename Value>
353 void BindRowWiseValue(SQLUSMALLINT column, void* base0, SQLLEN* indicators);
354
355 /// @brief Post-fetch fixup for one row-wise output column: resets each NULL row's @c std::optional to
356 /// @c std::nullopt (no-op for non-optional columns, whose value is materialized in place).
357 template <typename ValueType>
358 static void FinalizeRowWiseOutputColumn(void* base0,
359 std::size_t rowStride,
360 std::size_t rowCount,
361 SQLLEN const* indicators) noexcept;
362
363 template <SqlGetColumnNativeType T>
364 [[nodiscard]] std::optional<T> GetNullableColumn(SQLUSMALLINT column) const;
365
366 template <SqlGetColumnNativeType T>
367 [[nodiscard]] T GetColumnOr(SQLUSMALLINT column, T&& defaultValue) const;
368
369 LIGHTWEIGHT_API void RequireSuccess(SQLRETURN error,
370 std::source_location sourceLocation = std::source_location::current()) const;
371 LIGHTWEIGHT_API void PlanPostExecuteCallback(std::function<void()>&& cb) override;
372 LIGHTWEIGHT_API void PlanPostProcessOutputColumn(std::function<void()>&& cb) override;
373 [[nodiscard]] LIGHTWEIGHT_API SqlServerType ServerType() const noexcept override;
374 [[nodiscard]] LIGHTWEIGHT_API std::string const& DriverName() const noexcept override;
375 LIGHTWEIGHT_API void ProcessPostExecuteCallbacks();
376
377 LIGHTWEIGHT_API SQLLEN* ProvideInputIndicator() override;
378 LIGHTWEIGHT_API SQLLEN* ProvideInputIndicators(size_t rowCount) override;
379 LIGHTWEIGHT_API std::byte* ProvideBatchStagingBuffer(std::size_t byteCount) override;
380 LIGHTWEIGHT_API void ClearBatchIndicators();
381 /// Restores single-row, column-bound parameter binding (the ODBC default). @c noexcept so it can run
382 /// from a scope guard on the native-batch exception path.
383 LIGHTWEIGHT_API void ResetParameterArrayBinding() noexcept;
384 /// Throws unless @p result is a success code or @c SQL_NO_DATA (a searched UPDATE/DELETE that matched
385 /// no rows). Mirrors @c Execute() so the batch execute paths tolerate zero-row updates.
386 LIGHTWEIGHT_API void RequireExecuteSucceededOrNoData(
387 SQLRETURN result, std::source_location sourceLocation = std::source_location::current()) const;
388 /// Native-batch execute check: tolerates @c SQL_NO_DATA and, on success, verifies the driver
389 /// processed all @p expectedCount parameter sets (guards against silent partial array execution).
390 LIGHTWEIGHT_API void RequireSuccessfulBatchExecute(
391 SQLRETURN result,
392 SQLULEN processedCount,
393 SQLULEN expectedCount,
394 std::source_location sourceLocation = std::source_location::current()) const;
395 LIGHTWEIGHT_API void RequireIndicators();
396 LIGHTWEIGHT_API SQLLEN* GetIndicatorForColumn(SQLUSMALLINT column) noexcept;
397
398 // --- Transparent block-prefetch: backs the classic per-row fetch loops (FetchRow + GetColumn,
399 // bound output columns, SqlRowIterator, SqlVariantRowCursor) with the existing RowArrayCursor so a
400 // whole block of rows is materialized per SQLFetchScroll round-trip instead of one SQLFetch per row.
401 // Out-of-line accessors because the prefetch state lives in the opaque Data struct.
402
403 /// @return The effective prefetch depth: the connection default gated by the driver's row-array
404 /// capability (1 — i.e. disabled — when unsupported or the connection default is <= 1).
405 [[nodiscard]] std::size_t EffectivePrefetchDepth() const noexcept;
406 /// @brief Arms (or disables) block-prefetch on the first fetch of a result set; idempotent.
407 void ArmPrefetchOnFirstFetch() noexcept;
408 /// @brief Fetches the next logical row from the block buffer, refilling the block and running the
409 /// recorded bound-column scatters as needed. @return true if a row is available.
410 [[nodiscard]] std::expected<bool, SqlErrorInfo> FetchRowPrefetched() noexcept;
411 /// @return Whether block-prefetch is currently materializing this result set.
412 [[nodiscard]] LIGHTWEIGHT_API bool IsPrefetchActive() const noexcept;
413 /// @return The active block-prefetch cursor (precondition: @ref IsPrefetchActive).
414 [[nodiscard]] LIGHTWEIGHT_API RowArrayCursor const& PrefetchCursorRef() const noexcept;
415 /// @return The 0-based offset of the current logical row within the last fetched block.
416 [[nodiscard]] LIGHTWEIGHT_API std::size_t PrefetchRowInBlock() const noexcept;
417 /// @return Whether @ref BindOutputColumns should record scatter/deferred-bind closures (prefetch is
418 /// enabled and not yet disabled) instead of issuing @c SQLBindCol immediately.
419 [[nodiscard]] LIGHTWEIGHT_API bool ShouldRecordPrefetchBinding() const noexcept;
420 /// @brief Drops any previously recorded scatter/deferred-bind closures (for idempotent re-binding).
421 LIGHTWEIGHT_API void ResetPrefetchBindings() noexcept;
422 /// @brief Flags that a bound output column's target type cannot be served from the block buffer, so
423 /// arming must decline prefetch for this result set and keep the per-row path.
424 LIGHTWEIGHT_API void MarkPrefetchBindingUnsupported() noexcept;
425 /// @brief Records, for one output column, the per-row scatter closure (copies the current block cell
426 /// into the bound destination) and the real @c SQLBindCol thunk used if the result set turns out
427 /// prefetch-ineligible. Indexed by @p column so re-binding the same column overwrites rather than
428 /// appends — keeping the bound-column loop, the optional rebind idiom, and the DataMapper's per-row
429 /// re-binding all bounded.
430 /// @param column 1-based output column index.
431 /// @param scatter Copies the current block cell into the bound destination.
432 /// @param deferredBind Issues the real @c SQLBindCol when the fast path is declined.
433 LIGHTWEIGHT_API void RecordPrefetchColumn(SQLUSMALLINT column,
434 std::function<void()> scatter,
435 std::function<void()> deferredBind);
436 /// @brief Tears down all block-prefetch state, restoring the handle to single-row fetching.
437 LIGHTWEIGHT_API void ResetPrefetchState() noexcept;
438 /// @brief Builds an @c SqlVariant cell from the block buffer, mirroring @c SqlDataBinder<SqlVariant>.
439 [[nodiscard]] LIGHTWEIGHT_API SqlVariant MakePrefetchVariantCell(RowArrayCursor const& cursor,
440 std::size_t row,
441 SQLUSMALLINT column) const;
442 /// @brief Converts a materialized block cell to the requested native type @p T.
443 template <typename T>
444 [[nodiscard]] T ConvertCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column) const;
445
446 /// @brief Validates a 1-based column index against the active prefetch cursor, throwing
447 /// @c std::invalid_argument for an out-of-range index — matching the per-row path's behaviour for
448 /// an invalid descriptor index (ODBC SQLSTATE 07009).
449 LIGHTWEIGHT_API void RequirePrefetchColumnInRange(RowArrayCursor const& cursor, SQLUSMALLINT column) const;
450
451 /// @brief Records the scatter + deferred-bind closures for one bound output column @p arg of type
452 /// @p T (used instead of an immediate @c SQLBindCol while prefetch is pending/active).
453 template <SqlOutputColumnBinder T>
454 void RecordPrefetchOutputColumn(SQLUSMALLINT column, T* arg);
455
456 // private data members
457 struct Data;
458 std::unique_ptr<Data, void (*)(Data*)> m_data; // The private data of the statement
459 SqlConnection* m_connection {}; // Pointer to the connection object
460 SQLHSTMT m_hStmt {}; // The native oDBC statement handle
461 std::string m_preparedQuery; // The last prepared query
462 std::optional<SQLSMALLINT> m_numColumns; // The number of columns in the result set, if known
463 SQLSMALLINT m_expectedParameterCount {}; // The number of parameters expected by the query
464};
465
466/// API for reading an SQL query result set.
467class [[nodiscard]] SqlResultCursor
468{
469 public:
470 /// Constructs a result cursor for the given SQL statement.
471 explicit LIGHTWEIGHT_FORCE_INLINE SqlResultCursor(SqlStatement& stmt) noexcept:
472 m_stmt { &stmt }
473 {
474 }
475
476 SqlResultCursor() = delete;
477 SqlResultCursor(SqlResultCursor const&) = delete;
478 SqlResultCursor& operator=(SqlResultCursor const&) = delete;
479
480 /// Move constructor.
481 constexpr SqlResultCursor(SqlResultCursor&& other) noexcept:
482 m_stmt { other.m_stmt }
483 {
484 other.m_stmt = nullptr;
485 }
486
487 /// Move assignment operator.
488 constexpr SqlResultCursor& operator=(SqlResultCursor&& other) noexcept
489 {
490 if (this != &other)
491 {
492 m_stmt = other.m_stmt;
493 other.m_stmt = nullptr;
494 }
495 return *this;
496 }
497
498 LIGHTWEIGHT_FORCE_INLINE ~SqlResultCursor()
499 {
500 if (m_stmt)
501 {
502 m_stmt->CloseCursor();
503 m_stmt = nullptr;
504 }
505 }
506
507 /// Retrieves the number of rows affected by the last query.
508 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE size_t NumRowsAffected() const
509 {
510 return m_stmt->NumRowsAffected();
511 }
512
513 /// Retrieves the number of columns affected by the last query.
514 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE size_t NumColumnsAffected() const
515 {
516 return m_stmt->NumColumnsAffected();
517 }
518
519 /// Binds the given arguments to the prepared statement to store the fetched data to.
520 ///
521 /// The statement must be prepared before calling this function.
522 template <SqlOutputColumnBinder... Args>
523 LIGHTWEIGHT_FORCE_INLINE void BindOutputColumns(Args*... args)
524 {
525 m_stmt->BindOutputColumns(args...);
526 }
527
528 /// Binds a single output column at the given index to store fetched data.
529 template <SqlOutputColumnBinder T>
530 LIGHTWEIGHT_FORCE_INLINE void BindOutputColumn(SQLUSMALLINT columnIndex, T* arg)
531 {
532 m_stmt->BindOutputColumn(columnIndex, arg);
533 }
534
535 /// Fetches the next row of the result set.
536 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE bool FetchRow()
537 {
538 return m_stmt->FetchRow();
539 }
540
541 /// Attempts to fetch the next row, returning an error info on failure instead of throwing.
542 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE std::expected<bool, SqlErrorInfo> TryFetchRow(
543 std::source_location location = std::source_location::current()) noexcept
544 {
545 return m_stmt->TryFetchRow(location);
546 }
547
548 /// Binds the given records to the prepared statement to store the fetched data to.
549 template <typename... Records>
550 requires(((std::is_class_v<Records> && std::is_aggregate_v<Records>) && ...))
551 LIGHTWEIGHT_FORCE_INLINE void BindOutputColumnsToRecord(Records*... records)
552 {
553 m_stmt->BindOutputColumnsToRecord(records...);
554 }
555
556 /// @brief Fast bulk retrieval: materializes this result set into @p out via native ODBC row-wise
557 /// array fetch. Forwards to @c SqlStatement::FetchAllRowWise; see its contract (eligibility and
558 /// alignment preconditions are the caller's responsibility).
559 /// @param out Destination vector; results are appended.
560 /// @param arrayDepth Requested maximum rows per @c SQLFetchScroll round-trip.
561 /// @param accessors One invocable per result column; @c accessor(record) yields its mutable value.
562 template <typename Record, typename... ColumnAccessors>
563 LIGHTWEIGHT_FORCE_INLINE void FetchAllRowWise(std::vector<Record>& out,
564 std::size_t arrayDepth,
565 ColumnAccessors const&... accessors)
566 {
567 m_stmt->FetchAllRowWise(out, arrayDepth, accessors...);
568 }
569
570 /// Retrieves the value of the column at the given index for the currently selected row.
571 ///
572 /// Returns true if the value is not NULL, false otherwise.
573 template <SqlGetColumnNativeType T>
574 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE bool GetColumn(SQLUSMALLINT column, T* result) const
575 {
576 return m_stmt->GetColumn<T>(column, result);
577 }
578
579 /// Retrieves the value of the column at the given index for the currently selected row.
580 template <SqlGetColumnNativeType T>
581 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE T GetColumn(SQLUSMALLINT column) const
582 {
583 return m_stmt->GetColumn<T>(column);
584 }
585
586 /// Retrieves the value of the column at the given index for the currently selected row.
587 ///
588 /// If the value is NULL, std::nullopt is returned.
589 template <SqlGetColumnNativeType T>
590 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE std::optional<T> GetNullableColumn(SQLUSMALLINT column) const
591 {
592 return m_stmt->GetNullableColumn<T>(column);
593 }
594
595 /// Retrieves the value of the column at the given index for the currently selected row.
596 ///
597 /// If the value is NULL, the given @p defaultValue is returned.
598 template <SqlGetColumnNativeType T>
599 [[nodiscard]] T GetColumnOr(SQLUSMALLINT column, T&& defaultValue) const
600 {
601 return m_stmt->GetColumnOr(column, std::forward<T>(defaultValue));
602 }
603
604 private:
605 SqlStatement* m_stmt;
606};
607
608/// @brief Thrown by RowArrayCursor's constructor when the executed result set cannot be fixed-stride
609/// array-bound.
610///
611/// Raised for an unbounded/LOB or over-wide character column (e.g. a column the driver reports
612/// as SQL_LONGVARCHAR with no size, common for SQLite's dynamically-typed columns), or a query that
613/// produced no result columns. It is a precondition signal, not a database error, so callers that
614/// use bulk array-fetch purely as an optimization should catch it and fall back to the single-row
615/// path. Distinct from SqlException so transient-error retry logic does not mistake it for one.
616class RowArrayCursorUnsupported: public std::runtime_error
617{
618 public:
619 using std::runtime_error::runtime_error;
620};
621
622/// @brief A cursor that fetches result rows in bulk (ODBC row-array binding) for fast column reads.
623///
624/// Created via @ref SqlStatement::ExecuteBatchFetch. Instead of issuing one SQLGetData per cell,
625/// this cursor binds a contiguous buffer per result column and lets the driver materialize whole
626/// blocks of rows per SQLFetchScroll round-trip — eliminating per-cell driver round-trips.
627///
628/// Supported (fixed-stride) column types, decided per column from SQLDescribeCol:
629/// - integer SQL types (SQL_BIT, SQL_TINYINT, SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT)
630/// are bound as SQL_C_SBIGINT (an int64 buffer);
631/// - floating SQL types (SQL_REAL, SQL_FLOAT, SQL_DOUBLE) are bound as SQL_C_DOUBLE;
632/// - all other types (char/varchar/decimal/date/time/timestamp/numeric/...) are bound as
633/// SQL_C_CHAR with a per-column buffer sized from the reported column size (plus a margin,
634/// capped at @ref RowArrayCursor::MaxCharColumnBytes).
635///
636/// LOB / unbounded columns (the driver reports column size 0 or an absurdly large size) are
637/// rejected: constructing the cursor throws std::runtime_error. Such columns must use the
638/// single-row SQLGetData fallback instead.
639///
640/// The cursor is non-copyable and non-movable: it owns the ODBC statement's array-binding state for
641/// its entire lifetime. The constructor binds raw pointers into its own members
642/// (SQL_ATTR_ROWS_FETCHED_PTR, SQL_ATTR_ROW_STATUS_PTR) and SQLBindCol into its per-column buffers,
643/// so the object must not be relocated after construction — a move would leave the statement handle
644/// pointing at the moved-from storage (use-after-free). It is constructed in place via
645/// @ref SqlStatement::ExecuteBatchFetch (guaranteed copy elision) and used as a local. The bound
646/// buffers must outlive the SQLBindCol binding until fetching completes. Cell indices are 1-based to
647/// match SqlResultCursor::GetColumn.
648class [[nodiscard]] RowArrayCursor
649{
650 public:
651 /// Maximum byte width allocated for a single bound character column (per row). Columns whose
652 /// reported size exceeds this are treated as unbounded/LOB and rejected.
653 static constexpr std::size_t MaxCharColumnBytes = 8192;
654
655 /// Per-cursor byte budget for the bound column buffers. The effective array depth is
656 /// clamp(budget / row-byte-width, MinArrayDepth, requested depth), so wide tables (many or
657 /// large character columns) bind fewer rows per round-trip instead of exhausting memory —
658 /// the footprint otherwise multiplies across workers x columns x depth on real schemas.
659 static constexpr std::size_t MemoryBudgetBytes = 4 * 1024 * 1024;
660
661 /// Lower bound for the budget-adapted array depth, so bulk fetch always makes progress even
662 /// on extremely wide rows (never reduced below this unless the caller requested less).
663 static constexpr std::size_t MinArrayDepth = 16;
664
665 RowArrayCursor() = delete;
666 RowArrayCursor(RowArrayCursor const&) = delete;
667 RowArrayCursor& operator=(RowArrayCursor const&) = delete;
668 RowArrayCursor(RowArrayCursor&&) = delete;
669 RowArrayCursor& operator=(RowArrayCursor&&) = delete;
670
671 /// @brief Constructs the cursor on a statement whose query has already been executed.
672 /// Inspects the result columns via SQLDescribeCol, allocates per-column buffers, and binds
673 /// them with the row-array statement attributes.
674 /// @param stmt The executed statement (must outlive the cursor).
675 /// @param arrayDepth Maximum number of rows materialized per FetchArray() (must be > 0). The
676 /// effective depth may be reduced to fit MemoryBudgetBytes (see ArrayDepth()).
677 LIGHTWEIGHT_API RowArrayCursor(SqlStatement& stmt, std::size_t arrayDepth);
678
679 /// @brief Resets the statement's row-array attributes and unbinds the columns so the handle
680 /// can be safely reused.
681 LIGHTWEIGHT_API ~RowArrayCursor() noexcept;
682
683 /// @brief Fetches the next block of rows into the bound buffers.
684 /// @return The number of rows materialized (0 at end of result set).
685 [[nodiscard]] LIGHTWEIGHT_API std::size_t FetchArray();
686
687 /// @brief The number of result columns.
688 [[nodiscard]] LIGHTWEIGHT_API std::size_t ColumnCount() const noexcept;
689
690 /// @brief The effective maximum number of rows per FetchArray() — the requested depth, possibly
691 /// reduced so the bound buffers fit MemoryBudgetBytes (never below MinArrayDepth unless the
692 /// caller requested less).
693 [[nodiscard]] LIGHTWEIGHT_API std::size_t ArrayDepth() const noexcept;
694
695 /// @brief Reads an integer cell from the last fetched block.
696 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
697 /// @param column 1-based result column index.
698 /// @return The value, or std::nullopt if the cell is NULL.
699 [[nodiscard]] LIGHTWEIGHT_API std::optional<std::int64_t> GetI64(std::size_t rowInBatch, SQLUSMALLINT column) const;
700
701 /// @brief Reads a floating-point cell from the last fetched block.
702 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
703 /// @param column 1-based result column index.
704 /// @return The value, or std::nullopt if the cell is NULL.
705 [[nodiscard]] LIGHTWEIGHT_API std::optional<double> GetF64(std::size_t rowInBatch, SQLUSMALLINT column) const;
706
707 /// @brief Reads a text cell from the last fetched block, however the driver bound it.
708 ///
709 /// Narrow-bound cells (SQL_C_CHAR) are returned verbatim — identical bytes to a single-row
710 /// SQL_C_CHAR read. Wide-bound cells (the driver reported SQL_WCHAR/SQL_WVARCHAR, e.g. MSSQL
711 /// NVARCHAR, or SQLite which reports all text as wide) are converted UTF-16 -> UTF-8; for
712 /// valid UTF-8 source data that round-trip is byte-lossless, so the result again matches the
713 /// single-row read of the same cell.
714 ///
715 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
716 /// @param column 1-based result column index.
717 /// @return The UTF-8 value, or std::nullopt if the cell is NULL.
718 [[nodiscard]] LIGHTWEIGHT_API std::optional<std::string> GetString(std::size_t rowInBatch, SQLUSMALLINT column) const;
719
720 /// @brief Reads a DATE cell from the last fetched block. Valid only for Date-bound columns.
721 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
722 /// @param column 1-based result column index.
723 /// @return The value, or std::nullopt if the cell is NULL.
724 [[nodiscard]] LIGHTWEIGHT_API std::optional<SqlDate> GetDate(std::size_t rowInBatch, SQLUSMALLINT column) const;
725
726 /// @brief Reads a TIMESTAMP/DATETIME cell from the last fetched block. Valid only for
727 /// Timestamp-bound columns.
728 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
729 /// @param column 1-based result column index.
730 /// @return The value, or std::nullopt if the cell is NULL.
731 [[nodiscard]] LIGHTWEIGHT_API std::optional<SqlDateTime> GetTimestamp(std::size_t rowInBatch, SQLUSMALLINT column) const;
732
733 /// @brief Reads a GUID cell from the last fetched block. Valid only for Guid-bound columns
734 /// (drivers that report SQL_GUID, i.e. MSSQL uniqueidentifier / PostgreSQL uuid).
735 /// @param rowInBatch 0-based row offset within the block returned by the last FetchArray().
736 /// @param column 1-based result column index.
737 /// @return The value, or std::nullopt if the cell is NULL.
738 [[nodiscard]] LIGHTWEIGHT_API std::optional<SqlGuid> GetGuid(std::size_t rowInBatch, SQLUSMALLINT column) const;
739
740 /// @brief How a result column is bound for bulk fetch (the canonical fixed-stride C representation
741 /// chosen from the column's SQL type). Public so a transparent prefetch layer can dispatch a generic
742 /// cell read to the matching @c Get* accessor.
743 enum class BoundType : std::uint8_t
744 {
745 Int64, //!< bound as SQL_C_SBIGINT into an int64 buffer
746 Double, //!< bound as SQL_C_DOUBLE into a double buffer
747 Char, //!< bound as SQL_C_CHAR into a per-column byte buffer
748 WChar, //!< bound as SQL_C_WCHAR (UTF-16) into a per-column byte buffer
749 Date, //!< bound as SQL_C_TYPE_DATE into a SQL_DATE_STRUCT buffer
750 Timestamp, //!< bound as SQL_C_TYPE_TIMESTAMP into a SQL_TIMESTAMP_STRUCT buffer
751 Guid, //!< bound as SQL_C_GUID into a 16-byte GUID buffer
752 };
753
754 /// @brief The bound representation chosen for a result column.
755 /// @param column 1-based result column index.
756 /// @return The @ref BoundType the column was bound as.
757 [[nodiscard]] LIGHTWEIGHT_API BoundType ColumnBoundType(SQLUSMALLINT column) const;
758
759 /// @brief The raw SQL data type the driver reported for a result column (the @c SQL_* value from
760 /// @c SQLDescribeCol), letting callers gate on the exact source type rather than the coarser
761 /// @ref BoundType (which collapses e.g. textual TIME/NUMERIC into @c Char).
762 /// @param column 1-based result column index.
763 /// @return The reported @c SQL_* type code.
764 [[nodiscard]] LIGHTWEIGHT_API SQLSMALLINT ColumnSqlType(SQLUSMALLINT column) const;
765
766 /// @brief Whether a cell in the last fetched block is SQL NULL.
767 /// @param rowInBatch 0-based row offset within the block returned by the last @ref FetchArray.
768 /// @param column 1-based result column index.
769 /// @return @c true if the cell's length indicator is @c SQL_NULL_DATA.
770 [[nodiscard]] LIGHTWEIGHT_API bool IsCellNull(std::size_t rowInBatch, SQLUSMALLINT column) const;
771
772 private:
773 /// Per-column binding metadata + owning buffers.
774 struct BoundColumn
775 {
776 BoundType type {}; //!< how this column is bound
777 SQLSMALLINT sqlType {}; //!< raw SQL_* type reported by SQLDescribeCol
778 std::size_t elementWidth {}; //!< byte stride of one row's value in the buffer
779 std::vector<char> buffer; //!< arrayDepth * elementWidth contiguous bytes
780 std::vector<SQLLEN> indicators; //!< arrayDepth length indicators (SQL_NULL_DATA etc.)
781 };
782
783 void ResetStatementState() noexcept;
784
785 /// Shared accessor prelude: bounds-checks @p rowInBatch against the last fetched block,
786 /// verifies the column is bound as @p expected, and returns the cell's buffer address —
787 /// or nullptr when the cell is SQL NULL.
788 [[nodiscard]] char const* CheckedCell(std::size_t rowInBatch,
789 SQLUSMALLINT column,
790 BoundType expected,
791 char const* accessorName) const;
792
793 SqlStatement* m_stmt;
794 std::size_t m_arrayDepth;
795 std::size_t m_lastFetched = 0;
796 std::vector<BoundColumn> m_columns;
797 SQLULEN m_rowsFetched = 0;
798 std::vector<SQLUSMALLINT> m_rowStatus;
799};
800
801struct [[nodiscard]] SqlSentinelIterator
802{
803};
804
805class [[nodiscard]] SqlVariantRowIterator
806{
807 public:
808 explicit SqlVariantRowIterator(SqlSentinelIterator /*sentinel*/) noexcept:
809 _cursor { nullptr }
810 {
811 }
812
813 explicit SqlVariantRowIterator(SqlResultCursor& cursor) noexcept:
814 _numResultColumns { static_cast<SQLUSMALLINT>(cursor.NumColumnsAffected()) },
815 _cursor { &cursor }
816 {
817 _row.reserve(_numResultColumns);
818 ++(*this);
819 }
820
821 SqlVariantRow& operator*() noexcept
822 {
823 return _row;
824 }
825
826 SqlVariantRow const& operator*() const noexcept
827 {
828 return _row;
829 }
830
831 SqlVariantRowIterator& operator++() noexcept
832 {
833 _end = !_cursor->FetchRow();
834 if (!_end)
835 {
836 _row.clear();
837 for (auto const i: std::views::iota(SQLUSMALLINT(1), SQLUSMALLINT(_numResultColumns + 1)))
838 _row.emplace_back(_cursor->GetColumn<SqlVariant>(i));
839 }
840 return *this;
841 }
842
843 bool operator!=(SqlSentinelIterator /*sentinel*/) const noexcept
844 {
845 return !_end;
846 }
847
848 bool operator!=(SqlVariantRowIterator const& /*rhs*/) const noexcept
849 {
850 return !_end;
851 }
852
853 private:
854 bool _end = false;
855 SQLUSMALLINT _numResultColumns = 0;
856 SqlResultCursor* _cursor;
857 SqlVariantRow _row;
858};
859
860class [[nodiscard]] SqlVariantRowCursor
861{
862 public:
863 explicit SqlVariantRowCursor(SqlResultCursor&& cursor):
864 _resultCursor { std::move(cursor) }
865 {
866 }
867
868 SqlVariantRowIterator begin() noexcept
869 {
870 return SqlVariantRowIterator { _resultCursor };
871 }
872
873 static SqlSentinelIterator end() noexcept
874 {
875 return SqlSentinelIterator {};
876 }
877
878 private:
879 SqlResultCursor _resultCursor;
880};
881
882/// @brief SQL query result row iterator
883///
884/// Can be used to iterate over rows of the database and fetch them into a record type.
885/// @tparam T The record type to fetch the rows into.
886/// @code
887///
888/// struct MyRecord
889/// {
890/// Field<SqlGuid, PrimaryKey::AutoAssign> field1;
891/// Field<int> field2;
892/// Field<double> field3;
893/// };
894///
895/// for (auto const& row : SqlRowIterator<MyRecord>(stmt))
896/// {
897/// // row is of type MyRecord
898/// // row.field1, row.field2, row.field3 are accessible
899/// }
900/// @endcode
901template <typename T>
903{
904 public:
905 /// Constructs a row iterator using the given SQL connection.
907 _connection { &conn }
908 {
909 }
910
911 class iterator
912 {
913 public:
914 using difference_type = bool;
915 using value_type = T;
916
917 iterator& operator++()
918 {
919 if (_cursor)
920 {
921 _is_end = !_cursor->FetchRow();
922 return *this;
923 }
924 _is_end = true;
925 return *this;
926 }
927
928 LIGHTWEIGHT_FORCE_INLINE value_type operator*() noexcept
929 {
930 auto res = T {};
931
932 // begin() projects the record via Select().Fields<T>(), which emits one column per
933 // RecordColumnMember. Enumerate by column position rather than by member position, so that
934 // relation members (HasMany, HasManyThrough, HasOneThrough, ...) neither need a column nor
935 // shift the ones that follow them.
936 SQLUSMALLINT columnIndex = 0;
937 EnumerateRecordMembers(res, [this, &columnIndex]<size_t I, typename FieldType>(FieldType& value) {
938 if constexpr (RecordColumnMember<FieldType>)
939 {
940 ++columnIndex;
941 if constexpr (FieldWithStorage<FieldType>)
942 value = _cursor->GetColumn<typename FieldType::ValueType>(columnIndex);
943 else
944 value = _cursor->GetColumn<FieldType>(columnIndex);
945 }
946 });
947
948 return res;
949 }
950
951 LIGHTWEIGHT_FORCE_INLINE constexpr bool operator!=(iterator const& other) const noexcept
952 {
953 return _is_end != other._is_end;
954 }
955
956 constexpr iterator(std::default_sentinel_t /*sentinel*/) noexcept:
957 _is_end { true },
958 _cursor { std::nullopt }
959 {
960 }
961
962 explicit iterator(SqlConnection& conn):
963 _stmt { std::make_unique<SqlStatement>(conn) },
964 _cursor { std::nullopt }
965 {
966 }
967
968 LIGHTWEIGHT_FORCE_INLINE SqlStatement& Statement() noexcept
969 {
970 return *_stmt;
971 }
972
973 void SetCursor(SqlResultCursor cursor) noexcept
974 {
975 _cursor.emplace(std::move(cursor));
976 }
977
978 private:
979 bool _is_end = false;
980 std::unique_ptr<SqlStatement> _stmt;
981 std::optional<SqlResultCursor> _cursor;
982 };
983
984 /// Returns an iterator to the first row of the result set.
985 iterator begin()
986 {
987 auto it = iterator { *_connection };
988 auto& stmt = it.Statement();
989 stmt.Prepare(it.Statement().Query(RecordTableName<T>).Select().template Fields<T>().All());
990 it.SetCursor(stmt.Execute());
991 ++it;
992 return it;
993 }
994
995 /// Returns a sentinel iterator representing the end of the result set.
996 iterator end() noexcept
997 {
998 return iterator { std::default_sentinel };
999 }
1000
1001 private:
1002 SqlConnection* _connection;
1003};
1004
1005// {{{ inline implementation
1006inline LIGHTWEIGHT_FORCE_INLINE bool SqlStatement::IsAlive() const noexcept
1007{
1008 return m_connection && m_connection->IsAlive() && m_hStmt != nullptr;
1009}
1010
1011inline LIGHTWEIGHT_FORCE_INLINE bool SqlStatement::IsPrepared() const noexcept
1012{
1013 return !m_preparedQuery.empty();
1014}
1015
1016inline LIGHTWEIGHT_FORCE_INLINE SqlConnection& SqlStatement::Connection() noexcept
1017{
1018 return *m_connection;
1019}
1020
1021inline LIGHTWEIGHT_FORCE_INLINE SqlConnection const& SqlStatement::Connection() const noexcept
1022{
1023 return *m_connection;
1024}
1025
1026inline LIGHTWEIGHT_FORCE_INLINE SqlErrorInfo SqlStatement::LastError() const
1027{
1028 return SqlErrorInfo::FromStatementHandle(m_hStmt);
1029}
1030
1031inline LIGHTWEIGHT_FORCE_INLINE SQLHSTMT SqlStatement::NativeHandle() const noexcept
1032{
1033 return m_hStmt;
1034}
1035
1036inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::Prepare(SqlQueryObject auto const& queryObject) &
1037{
1038 Prepare(queryObject.ToSql());
1039}
1040
1041inline LIGHTWEIGHT_FORCE_INLINE SqlStatement SqlStatement::Prepare(SqlQueryObject auto const& queryObject) &&
1042{
1043 return Prepare(queryObject.ToSql());
1044}
1045
1046inline LIGHTWEIGHT_FORCE_INLINE std::string const& SqlStatement::PreparedQuery() const noexcept
1047{
1048 return m_preparedQuery;
1049}
1050
1051/// @brief Out-of-line definition of `SqlStatement::BindOutputColumns`.
1052template <SqlOutputColumnBinder... Args>
1053inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::BindOutputColumns(Args*... args)
1054{
1055 if (ShouldRecordPrefetchBinding())
1056 {
1057 // Prefetch is pending/active: defer the SQLBindCol and instead record per-column scatters that
1058 // copy each block cell into the caller's storage. ResetPrefetchBindings makes the optional
1059 // rebind idiom (re-calling BindOutputColumns each row) idempotent rather than accumulating.
1060 ResetPrefetchBindings();
1061 SQLUSMALLINT i = 0;
1062 ((++i, RecordPrefetchOutputColumn<Args>(i, args)), ...);
1063 return;
1064 }
1065
1066 RequireIndicators();
1067
1068 SQLUSMALLINT i = 0;
1069 ((++i, RequireSuccess(SqlDataBinder<Args>::OutputColumn(m_hStmt, i, args, GetIndicatorForColumn(i), *this))), ...);
1070}
1071
1072template <typename... Records>
1073 requires(((std::is_class_v<Records> && std::is_aggregate_v<Records>) && ...))
1074void SqlStatement::BindOutputColumnsToRecord(Records*... records)
1075{
1076 if (ShouldRecordPrefetchBinding())
1077 {
1078 ResetPrefetchBindings();
1079 SQLUSMALLINT i = 0;
1080 ((EnumerateRecordMembers(*records,
1081 [this, &i]<size_t I, typename FieldType>(FieldType& value) {
1082 // Only members mapping onto a column occupy a result set index.
1083 if constexpr (RecordColumnMember<FieldType>)
1084 {
1085 ++i;
1086 this->RecordPrefetchOutputColumn<FieldType>(i, &value);
1087 }
1088 })),
1089 ...);
1090 return;
1091 }
1092
1093 RequireIndicators();
1094
1095 SQLUSMALLINT i = 0;
1096 ((EnumerateRecordMembers(*records,
1097 [this, &i]<size_t I, typename FieldType>(FieldType& value) {
1098 // Only members mapping onto a column occupy a result set index.
1099 if constexpr (RecordColumnMember<FieldType>)
1100 {
1101 ++i;
1102 RequireSuccess(SqlDataBinder<FieldType>::OutputColumn(
1103 m_hStmt, i, &value, GetIndicatorForColumn(i), *this));
1104 }
1105 })),
1106 ...);
1107}
1108
1109/// @brief Out-of-line definition of `SqlStatement::BindOutputColumn`.
1110template <SqlOutputColumnBinder T>
1111inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::BindOutputColumn(SQLUSMALLINT columnIndex, T* arg)
1112{
1113 // Singular bind: no ResetPrefetchBindings (callers — e.g. the DataMapper — set columns one at a
1114 // time); RecordPrefetchColumn overwrites the column's slot so per-row re-binding stays bounded.
1115 if (ShouldRecordPrefetchBinding())
1116 {
1117 RecordPrefetchOutputColumn<T>(columnIndex, arg);
1118 return;
1119 }
1120
1121 RequireIndicators();
1122
1123 RequireSuccess(SqlDataBinder<T>::OutputColumn(m_hStmt, columnIndex, arg, GetIndicatorForColumn(columnIndex), *this));
1124}
1125
1126/// @copydoc SqlStatement::BindInputParameter(SQLSMALLINT, Arg const&)
1127template <SqlInputParameterBinder Arg>
1128inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::BindInputParameter(SQLSMALLINT columnIndex, Arg const& arg)
1129{
1130 // tell Execute() that we don't know the expected count
1131 m_expectedParameterCount = (std::numeric_limits<decltype(m_expectedParameterCount)>::max)();
1132 RequireSuccess(SqlDataBinder<Arg>::InputParameter(m_hStmt, static_cast<SQLUSMALLINT>(columnIndex), arg, *this));
1133}
1134
1135/// @copydoc SqlStatement::BindInputParameter(SQLSMALLINT, Arg const&, ColumnName&&)
1136template <SqlInputParameterBinder Arg, typename ColumnName>
1137inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::BindInputParameter(SQLSMALLINT columnIndex,
1138 Arg const& arg,
1139 ColumnName&& columnNameHint)
1140{
1141 SqlLogger::GetLogger().OnBindInputParameter(std::forward<ColumnName>(columnNameHint), arg);
1142 BindInputParameter(columnIndex, arg);
1143}
1144
1145template <SqlInputParameterBinder... Args>
1146SqlResultCursor SqlStatement::Execute(Args const&... args)
1147{
1148 // Each input parameter must have an address,
1149 // such that we can call SQLBindParameter() without needing to copy it.
1150 // The memory region behind the input parameter must exist until the SQLExecute() call.
1151
1152 ZoneScopedN("SqlStatement::Execute");
1153 ZoneTextObject(m_preparedQuery);
1154 SqlLogger::GetLogger().OnExecute(m_preparedQuery);
1155
1156 if (!(m_expectedParameterCount == (std::numeric_limits<decltype(m_expectedParameterCount)>::max)()
1157 && sizeof...(args) == 0)
1158 && !(m_expectedParameterCount == sizeof...(args)))
1159 throw std::invalid_argument { "Invalid argument count" };
1160
1161 SQLUSMALLINT i = 0;
1162 ((++i,
1163 SqlLogger::GetLogger().OnBindInputParameter({}, args),
1164 RequireSuccess(SqlDataBinder<Args>::InputParameter(m_hStmt, i, args, *this))),
1165 ...);
1166
1167 auto const result = SQLExecute(m_hStmt);
1168
1169 if (result != SQL_NO_DATA && result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO)
1170 throw SqlException(SqlErrorInfo::FromStatementHandle(m_hStmt), std::source_location::current());
1171
1172 ProcessPostExecuteCallbacks();
1173 return SqlResultCursor { *this };
1174}
1175
1176// clang-format off
1177template <typename T>
1178concept SqlNativeContiguousValueConcept =
1179 std::same_as<T, bool>
1180 || std::same_as<T, char>
1181 || std::same_as<T, unsigned char>
1182 || std::same_as<T, wchar_t>
1183 || std::same_as<T, std::int16_t>
1184 || std::same_as<T, std::uint16_t>
1185 || std::same_as<T, std::int32_t>
1186 || std::same_as<T, std::uint32_t>
1187 || std::same_as<T, std::int64_t>
1188 || std::same_as<T, std::uint64_t>
1189 || std::same_as<T, float>
1190 || std::same_as<T, double>
1191 || std::same_as<T, SqlDate>
1192 || std::same_as<T, SqlTime>
1193 || std::same_as<T, SqlDateTime>
1194 || std::same_as<T, SqlFixedString<T::Capacity, typename T::value_type, T::PostRetrieveOperation>>;
1195
1196template <typename FirstColumnBatch, typename... MoreColumnBatches>
1197concept SqlNativeBatchable =
1198 std::ranges::contiguous_range<FirstColumnBatch>
1199 && (std::ranges::contiguous_range<MoreColumnBatches> && ...)
1200 && SqlNativeContiguousValueConcept<std::ranges::range_value_t<FirstColumnBatch>>
1201 && (SqlNativeContiguousValueConcept<std::ranges::range_value_t<MoreColumnBatches>> && ...);
1202
1203// clang-format on
1204
1205/// @brief A value type that can be bound in a native ODBC row-wise parameter array (fixed-width,
1206/// inline, indicator-free, bound identically across backends). Backed by the data-driven
1207/// @c SqlIsNativeRowBindableValue trait that each eligible binder header opts into.
1208template <typename V>
1209concept SqlNativeRowBindableValue = SqlIsNativeRowBindableValue<V>;
1210
1211/// @brief A @c std::optional column that can be bound zero-copy in a native row-wise batch: the
1212/// contained type is row-bindable and non-numeric (numeric optionals are not bound at a uniform
1213/// offset/representation across backends and therefore use the soft path).
1214template <typename V>
1216 SqlIsStdOptional<V> && SqlNativeRowBindableValue<typename V::value_type> && !SqlIsNumericValue<typename V::value_type>;
1217
1218/// @brief A column value type usable on the native row-wise batch path — either a row-bindable fixed
1219/// value or a row-bindable optional of one.
1220template <typename V>
1222
1223/// @brief A column usable on the native row-wise array-FETCH fast path. Intentionally identical to the
1224/// write-side @c SqlRowBindableColumn — the set of types we can bind row-wise into a record block on
1225/// fetch matches the set we can bind row-wise as a parameter array on execute: fixed-width primitives,
1226/// date/time/datetime, numeric, char-based fixed-capacity strings, and non-numeric optionals of those.
1227///
1228/// Char fixed strings are materialized by a dedicated SQL_C_CHAR bind plus a per-row length/trim fixup
1229/// (see @c BindRowWiseOutputColumn / @c FinalizeRowWiseOutputColumn); on PostgreSQL, whose driver
1230/// transcodes SQL_C_CHAR through the client codepage, records carrying one fall back to the per-row
1231/// (wide) path instead — see @c SqlConnection::RoundTripsNarrowTextByteExact. Growable strings/binary,
1232/// GUID and variant are not row-bindable and make the whole record fall back to the per-row fetch path.
1233template <typename V>
1235
1236/// @brief Whether @p V's binder provides a row-wise batch entry point (@c BatchRowWiseInputParameter).
1237///
1238/// Such types (e.g. @c std::optional of a fixed type, or inline fixed-capacity strings) need a
1239/// temporary row-strided NULL/length indicator buffer, which in turn requires the row stride to keep
1240/// @c SQLLEN indicator slots aligned. Plain indicator-free fixed values bind via @c InputParameter and
1241/// do not satisfy this concept.
1242template <typename V>
1244 requires(SQLHSTMT stmt, SQLUSMALLINT column, V const* elem0, std::size_t n, SqlDataBinderCallback& cb) {
1245 { SqlDataBinder<V>::BatchRowWiseInputParameter(stmt, column, elem0, n, n, cb) } -> std::same_as<SQLRETURN>;
1246 };
1247
1248template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::contiguous_range... MoreColumnBatches>
1249SqlResultCursor SqlStatement::ExecuteBatchNative(FirstColumnBatch const& firstColumnBatch,
1250 MoreColumnBatches const&... moreColumnBatches)
1251{
1252 static_assert(SqlNativeBatchable<FirstColumnBatch, MoreColumnBatches...>,
1253 "Must be a supported native contiguous element type.");
1254
1255 ZoneScopedN("SqlStatement::ExecuteBatchNative");
1256 ZoneTextObject(m_preparedQuery);
1257
1258 if (m_expectedParameterCount != 1 + sizeof...(moreColumnBatches))
1259 throw std::invalid_argument { "Invalid number of columns" };
1260
1261 auto const rowCount = std::ranges::size(firstColumnBatch);
1262 ZoneValue(rowCount);
1263 if (!((std::size(moreColumnBatches) == rowCount) && ...))
1264 throw std::invalid_argument { "Uneven number of rows" };
1265
1266 size_t rowStart = 0;
1267
1268 // clang-format off
1269 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1270 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER) rowCount, 0));
1271 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, &rowStart, 0));
1272 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_BIND_TYPE, SQL_PARAM_BIND_BY_COLUMN, 0));
1273 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_OPERATION_PTR, SQL_PARAM_PROCEED, 0));
1274 ClearBatchIndicators();
1275 RequireSuccess(SqlDataBinder<std::remove_cvref_t<decltype(*std::ranges::data(firstColumnBatch))>>::
1276 BatchInputParameter(m_hStmt, 1, std::ranges::data(firstColumnBatch), rowCount, *this));
1277 SQLUSMALLINT column = 1;
1278 (RequireSuccess(SqlDataBinder<std::remove_cvref_t<decltype(*std::ranges::data(moreColumnBatches))>>::
1279 BatchInputParameter(m_hStmt, ++column, std::ranges::data(moreColumnBatches), rowCount, *this)),
1280 ...);
1281 RequireSuccess(SQLExecute(m_hStmt));
1282 ProcessPostExecuteCallbacks();
1283 // clang-format on
1284 return SqlResultCursor { *this };
1285}
1286
1287/// @copydoc SqlStatement::ExecuteBatch
1288template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::range... MoreColumnBatches>
1289inline LIGHTWEIGHT_FORCE_INLINE SqlResultCursor SqlStatement::ExecuteBatch(FirstColumnBatch const& firstColumnBatch,
1290 MoreColumnBatches const&... moreColumnBatches)
1291{
1292 // If the input ranges are contiguous and their element types are contiguous and supported as well,
1293 // we can use the native batch execution.
1294 if constexpr (SqlNativeBatchable<FirstColumnBatch, MoreColumnBatches...>)
1295 return ExecuteBatchNative(firstColumnBatch, moreColumnBatches...);
1296 else
1297 return ExecuteBatchSoft(firstColumnBatch, moreColumnBatches...);
1298}
1299
1300template <SqlInputParameterBatchBinder FirstColumnBatch, std::ranges::range... MoreColumnBatches>
1301SqlResultCursor SqlStatement::ExecuteBatchSoft(FirstColumnBatch const& firstColumnBatch,
1302 MoreColumnBatches const&... moreColumnBatches)
1303{
1304 ZoneScopedN("SqlStatement::ExecuteBatchSoft");
1305 ZoneTextObject(m_preparedQuery);
1306
1307 if (m_expectedParameterCount != 1 + sizeof...(moreColumnBatches))
1308 throw std::invalid_argument { "Invalid number of columns" };
1309
1310 auto const rowCount = std::ranges::size(firstColumnBatch);
1311 ZoneValue(rowCount);
1312 if (!((std::size(moreColumnBatches) == rowCount) && ...))
1313 throw std::invalid_argument { "Uneven number of rows" };
1314
1315 for (auto const rowIndex: std::views::iota(size_t { 0 }, rowCount))
1316 {
1317 std::apply(
1318 [&]<SqlInputParameterBinder... ColumnValues>(ColumnValues const&... columnsInRow) {
1319 SQLUSMALLINT column = 0;
1320 ((++column, SqlDataBinder<ColumnValues>::InputParameter(m_hStmt, column, columnsInRow, *this)), ...);
1321 RequireSuccess(SQLExecute(m_hStmt));
1322 ProcessPostExecuteCallbacks();
1323 },
1324 std::make_tuple(
1325 std::ref(*std::ranges::next(std::ranges::begin(firstColumnBatch), static_cast<std::ptrdiff_t>(rowIndex))),
1326 std::ref(
1327 *std::ranges::next(std::ranges::begin(moreColumnBatches), static_cast<std::ptrdiff_t>(rowIndex)))...));
1328 }
1329 return SqlResultCursor { *this };
1330}
1331
1332template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
1333 requires(sizeof...(ColumnAccessors) >= 1
1334 && (std::invocable<ColumnAccessors const&, std::ranges::range_value_t<Rows> const&> && ...))
1335SqlResultCursor SqlStatement::ExecuteBatch(Rows const& rows, ColumnAccessors const&... accessors)
1336{
1337 ZoneScopedN("SqlStatement::ExecuteBatch(row-major)");
1338 ZoneTextObject(m_preparedQuery);
1339
1340 using RowElem = std::ranges::range_value_t<Rows>;
1341
1342 auto const rowCount = std::ranges::size(rows);
1343 if (rowCount == 0)
1344 return SqlResultCursor { *this };
1345
1346 if (m_expectedParameterCount != static_cast<SQLSMALLINT>(sizeof...(accessors)))
1347 throw std::invalid_argument { "Invalid number of columns" };
1348
1349 // Compile-time eligibility for the native row-wise path: every column must be row-bindable, every
1350 // accessor must return an lvalue reference (so the bound address is a stable subobject), and — when
1351 // any column needs a row-strided indicator (optionals, inline fixed-capacity strings) — the row
1352 // stride must keep SQLLEN indicator slots aligned and non-overlapping.
1353 constexpr bool allColumnsRowBindable =
1355 constexpr bool allAccessorsReturnReference =
1356 (std::is_reference_v<std::invoke_result_t<ColumnAccessors const&, RowElem const&>> && ...);
1357 constexpr bool anyStridedIndicatorColumn =
1359 constexpr bool indicatorAlignmentSatisfied = (sizeof(RowElem) % alignof(SQLLEN)) == 0;
1360
1361 if constexpr (allColumnsRowBindable && allAccessorsReturnReference
1362 && (!anyStridedIndicatorColumn || indicatorAlignmentSatisfied))
1363 {
1364 auto const* rowData = std::ranges::data(rows);
1365
1366 // Runtime guard: confirm each accessor yields a constant-offset subobject (stride == sizeof row),
1367 // so binding row 0's address and striding by sizeof(RowElem) addresses every row correctly.
1368 auto const accessorStrideMatchesRow = [&](auto const& accessor) noexcept -> bool {
1369 auto const* first = reinterpret_cast<std::byte const*>(std::addressof(accessor(rowData[0])));
1370 auto const* second = reinterpret_cast<std::byte const*>(std::addressof(accessor(rowData[1])));
1371 return static_cast<std::size_t>(second - first) == sizeof(RowElem);
1372 };
1373 bool const rowStrideOk = rowCount < 2 || (accessorStrideMatchesRow(accessors) && ...);
1374
1375 if (m_connection->SupportsNativeRowBatch() && rowStrideOk)
1376 return ExecuteBatchNativeRowWise(rows, accessors...);
1377 }
1378
1379 return ExecuteBatchSoftRowMajor(rows, accessors...);
1380}
1381
1382template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
1383SqlResultCursor SqlStatement::ExecuteBatchNativeRowWise(Rows const& rows, ColumnAccessors const&... accessors)
1384{
1385 ZoneScopedN("SqlStatement::ExecuteBatchNativeRowWise");
1386 ZoneTextObject(m_preparedQuery);
1387
1388 using RowElem = std::ranges::range_value_t<Rows>;
1389 auto const rowCount = std::ranges::size(rows);
1390 ZoneValue(rowCount);
1391 auto const* rowData = std::ranges::data(rows);
1392
1393 // Optimistic init: a driver that ignores SQL_ATTR_PARAMS_PROCESSED_PTR leaves this == rowCount, so the
1394 // post-execute completeness check never false-trips on such a driver.
1395 SQLULEN processedCount = rowCount;
1396
1397 // Restore single-row binding and release scratch buffers on EVERY exit — success or exception — so a
1398 // throwing bind/execute can never leave the handle in a stale multi-paramset/row-wise state for a
1399 // later reuse (e.g. a single Execute() without re-Prepare). Installed before the attributes are set,
1400 // so a failure mid-setup is unwound too.
1401 auto const restoreParameterBinding = detail::Finally([this] {
1402 ResetParameterArrayBinding();
1403 ClearBatchIndicators();
1404 });
1405
1406 // Row-wise array binding: the driver strides every bound value and indicator pointer by sizeof(RowElem).
1407 // clang-format off
1408 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1409 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER) rowCount, 0));
1410 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1411 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_BIND_TYPE, (SQLPOINTER) sizeof(RowElem), 0));
1412 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_BIND_OFFSET_PTR, nullptr, 0));
1413 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAM_OPERATION_PTR, SQL_PARAM_PROCEED, 0));
1414 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_PARAMS_PROCESSED_PTR, &processedCount, 0));
1415 // clang-format on
1416
1417 SQLUSMALLINT column = 0;
1418 auto const bindColumn = [&](auto const& accessor) {
1419 ++column;
1420 using ValueType = std::remove_cvref_t<decltype(accessor(rowData[0]))>;
1421 // Types needing a per-row indicator (optionals, inline fixed-capacity strings) provide a
1422 // row-wise batch binder; indicator-free fixed values bind directly via InputParameter.
1423 if constexpr (SqlHasRowWiseBatchBinder<ValueType>)
1424 RequireSuccess(SqlDataBinder<ValueType>::BatchRowWiseInputParameter(
1425 m_hStmt, column, std::addressof(accessor(rowData[0])), sizeof(RowElem), rowCount, *this));
1426 else
1427 RequireSuccess(SqlDataBinder<ValueType>::InputParameter(m_hStmt, column, accessor(rowData[0]), *this));
1428 };
1429 (bindColumn(accessors), ...);
1430
1431 SqlLogger::GetLogger().OnExecuteBatch();
1432 // Capture the result before reading processedCount: SQLExecute updates it via the bound pointer, and
1433 // function-argument evaluation order is unspecified.
1434 auto const executeResult = SQLExecute(m_hStmt);
1435 RequireSuccessfulBatchExecute(executeResult, processedCount, static_cast<SQLULEN>(rowCount));
1436 ProcessPostExecuteCallbacks();
1437
1438 return SqlResultCursor { *this };
1439}
1440
1441template <std::ranges::contiguous_range Rows, typename... ColumnAccessors>
1442SqlResultCursor SqlStatement::ExecuteBatchSoftRowMajor(Rows const& rows, ColumnAccessors const&... accessors)
1443{
1444 ZoneScopedN("SqlStatement::ExecuteBatchSoftRowMajor");
1445 ZoneTextObject(m_preparedQuery);
1446
1447 auto const* rowData = std::ranges::data(rows);
1448 auto const rowCount = std::ranges::size(rows);
1449 ZoneValue(rowCount);
1450
1451 for (auto const rowIndex: std::views::iota(std::size_t { 0 }, rowCount))
1452 {
1453 auto const& row = rowData[rowIndex];
1454 SQLUSMALLINT column = 0;
1455 ((++column,
1456 RequireSuccess(SqlDataBinder<std::remove_cvref_t<decltype(accessors(row))>>::InputParameter(
1457 m_hStmt, column, accessors(row), *this))),
1458 ...);
1459 SqlLogger::GetLogger().OnExecute(m_preparedQuery);
1460 RequireExecuteSucceededOrNoData(SQLExecute(m_hStmt));
1461 ProcessPostExecuteCallbacks();
1462 }
1463
1464 return SqlResultCursor { *this };
1465}
1466
1467template <typename Value>
1468void SqlStatement::BindRowWiseValue(SQLUSMALLINT column, void* base0, SQLLEN* indicators)
1469{
1470 if constexpr (IsSqlFixedString<Value>)
1471 {
1472 // Char fixed-capacity strings are stored inline, so each row's character buffer is reached at
1473 // Data(row0) + i*rowStride. Bind it as SQL_C_CHAR with the Capacity(+NUL) buffer length (matching
1474 // the non-PostgreSQL single-row OutputColumn); FinalizeRowWiseOutputColumn sets each row's length
1475 // from its indicator and applies the trailing-whitespace/NUL trim. PostgreSQL never reaches here:
1476 // such records take the per-row (wide) path (see SqlConnection::RoundTripsNarrowTextByteExact).
1477 RequireSuccess(SQLBindCol(m_hStmt,
1478 column,
1479 SQL_C_CHAR,
1480 (SQLPOINTER) SqlBasicStringOperations<Value>::Data(static_cast<Value*>(base0)),
1481 static_cast<SQLLEN>(Value::Capacity) + 1,
1482 indicators));
1483 }
1484 else
1485 {
1486 // Fixed-width value (primitive, date/time/datetime, numeric): a plain, callback-free SQLBindCol
1487 // straight into the record field; the driver strides by rowStride.
1488 RequireSuccess(SqlDataBinder<Value>::OutputColumn(m_hStmt, column, static_cast<Value*>(base0), indicators, *this));
1489 }
1490}
1491
1492template <typename ValueType>
1493SQLLEN* SqlStatement::BindRowWiseOutputColumn(SQLUSMALLINT column, void* base0, std::size_t rowStride, std::size_t depth)
1494{
1495 // Row-wise binding strides the indicator pointer by SQL_ATTR_ROW_BIND_TYPE (== rowStride), the same
1496 // as the value pointer; there is no separate indicator stride. So the indicator array over-allocates
1497 // to rowStride per row (only sizeof(SQLLEN) of each slot is used) — intrinsic to ODBC row-wise
1498 // binding, identical to the write side (see SqlDataBinderCallback::ProvideBatchStagingBuffer).
1499 auto* const indicatorBytes = ProvideBatchStagingBuffer(((depth - 1) * rowStride) + sizeof(SQLLEN));
1500 auto* const indicators = reinterpret_cast<SQLLEN*>(indicatorBytes);
1501
1502 if constexpr (SqlIsStdOptional<ValueType>)
1503 {
1504 using Inner = ValueType::value_type;
1505 auto* const optBytes = static_cast<std::byte*>(base0);
1506 // Pre-engage every row's optional so its contained storage is valid to bind into; rows that come
1507 // back NULL are reset to std::nullopt in FinalizeRowWiseOutputColumn.
1508 for (auto const i: std::views::iota(std::size_t { 0 }, depth))
1509 reinterpret_cast<ValueType*>(optBytes + (i * rowStride))->emplace();
1510 // The contained value of row 0 (constant offset within every optional); the driver strides it by
1511 // rowStride to reach each row's contained storage in place.
1512 auto* const contained0 = reinterpret_cast<Inner*>(optBytes + detail::OptionalValueOffset<Inner>());
1513 BindRowWiseValue<Inner>(column, contained0, indicators);
1514 }
1515 else
1516 {
1517 BindRowWiseValue<ValueType>(column, base0, indicators);
1518 }
1519 return indicators;
1520}
1521
1522template <typename ValueType>
1523void SqlStatement::FinalizeRowWiseOutputColumn(void* base0,
1524 std::size_t rowStride,
1525 std::size_t rowCount,
1526 SQLLEN const* indicators) noexcept
1527{
1528 auto const indicatorAt = [&](std::size_t i) noexcept {
1529 return *reinterpret_cast<SQLLEN const*>(reinterpret_cast<std::byte const*>(indicators) + (i * rowStride));
1530 };
1531
1532 if constexpr (SqlIsStdOptional<ValueType>)
1533 {
1534 using Inner = ValueType::value_type;
1535 auto* const optBytes = static_cast<std::byte*>(base0);
1536 for (auto const i: std::views::iota(std::size_t { 0 }, rowCount))
1537 {
1538 auto* const optional = reinterpret_cast<ValueType*>(optBytes + (i * rowStride));
1539 if (indicatorAt(i) == SQL_NULL_DATA)
1540 optional->reset();
1541 else if constexpr (IsSqlFixedString<Inner>)
1542 {
1543 // Engaged char fixed string: set its length and trim, matching the single-row binder.
1544 // BindRowWiseOutputColumn pre-engages every row and only the NULL branch above ever
1545 // disengages one, so this holds unconditionally — tested anyway to keep the access
1546 // provably safe rather than invariant-dependent.
1547 if (optional->has_value())
1548 SqlBasicStringOperations<Inner>::PostProcessOutputColumn(std::addressof(**optional), indicatorAt(i));
1549 }
1550 // Engaged fixed-width inner: already materialized in place, nothing more to do.
1551 }
1552 }
1553 else if constexpr (IsSqlFixedString<ValueType>)
1554 {
1555 auto* const base = static_cast<std::byte*>(base0);
1556 for (auto const i: std::views::iota(std::size_t { 0 }, rowCount))
1557 SqlBasicStringOperations<ValueType>::PostProcessOutputColumn(
1558 reinterpret_cast<ValueType*>(base + (i * rowStride)), indicatorAt(i));
1559 }
1560 // Plain fixed-width non-optional columns: the value is materialized in place; a NULL leaves the
1561 // default-constructed value untouched, matching the single-row bound-output path.
1562}
1563
1564template <typename Record, typename... ColumnAccessors>
1565void SqlStatement::FetchAllRowWise(std::vector<Record>& out, std::size_t arrayDepth, ColumnAccessors const&... accessors)
1566{
1567 ZoneScopedN("SqlStatement::FetchAllRowWise");
1568 ZoneTextObject(m_preparedQuery);
1569
1570 static_assert(sizeof...(ColumnAccessors) >= 1, "FetchAllRowWise requires at least one column accessor");
1571 constexpr std::size_t columnCount = sizeof...(ColumnAccessors);
1572
1573 // Adapt the depth to the per-cursor memory budget. The row-strided indicator staging over-allocates
1574 // to sizeof(Record) per row per column, so the per-row footprint is sizeof(Record) * (1 + columns)
1575 // (data block + one indicator buffer per column). Clamp like RowArrayCursor so wide rows bind fewer
1576 // rows per round-trip instead of exhausting memory.
1577 {
1578 auto const perRow = sizeof(Record) * (1 + columnCount);
1579 auto const budgetDepth = RowArrayCursor::MemoryBudgetBytes / std::max<std::size_t>(perRow, 1);
1580 auto const minDepth = std::min(RowArrayCursor::MinArrayDepth, arrayDepth); // never raise above the request
1581 arrayDepth = std::clamp(budgetDepth, minDepth, arrayDepth);
1582 }
1583
1584 std::vector<SQLUSMALLINT> rowStatus(arrayDepth);
1585 SQLULEN rowsFetched = 0;
1586
1587 // Restore single-row, column-bound fetch state and release staging buffers on EVERY exit — success or
1588 // exception — so a throwing bind/fetch can never leave the handle in a stale row-array state for a
1589 // later reuse. Mirrors ExecuteBatchNativeRowWise's restoreParameterBinding guard.
1590 auto const restoreFetchState = detail::Finally([this] {
1591 SQLFreeStmt(m_hStmt, SQL_UNBIND);
1592 // clang-format off
1593 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1594 SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER) 1, 0);
1595 SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_BIND_TYPE, SQL_BIND_BY_COLUMN, 0);
1596 SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_STATUS_PTR, nullptr, 0);
1597 SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROWS_FETCHED_PTR, nullptr, 0);
1598 // clang-format on
1599 ClearBatchIndicators();
1600 });
1601
1602 // clang-format off
1603 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1604 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_BIND_TYPE, (SQLPOINTER) sizeof(Record), 0));
1605 // NOLINTNEXTLINE(performance-no-int-to-ptr)
1606 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER) arrayDepth, 0));
1607 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROW_STATUS_PTR, rowStatus.data(), 0));
1608 RequireSuccess(SQLSetStmtAttr(m_hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &rowsFetched, 0));
1609 // clang-format on
1610
1611 for (;;)
1612 {
1613 std::size_t const base = out.size();
1614 out.resize(base + arrayDepth);
1615 Record* const row0 = out.data() + base;
1616
1617 // Rebind each column into this block's records (the value pointer follows out's storage across a
1618 // reallocation) and refresh the per-column row-strided indicator buffers.
1619 ClearBatchIndicators();
1620 std::array<SQLLEN*, columnCount> indicators {};
1621 SQLUSMALLINT column = 0;
1622 std::size_t bindIndex = 0;
1623 ((indicators[bindIndex++] = BindRowWiseOutputColumn<std::remove_cvref_t<decltype(accessors(*row0))>>(
1624 ++column, std::addressof(accessors(*row0)), sizeof(Record), arrayDepth)),
1625 ...);
1626
1627 rowsFetched = 0;
1628 auto const fetchResult = SQLFetchScroll(m_hStmt, SQL_FETCH_NEXT, 0);
1629 if (fetchResult == SQL_NO_DATA)
1630 {
1631 out.resize(base);
1632 break;
1633 }
1634 // SQL_SUCCESS_WITH_INFO is acceptable: rowsFetched stays valid. The fixed-width eligibility gate
1635 // keeps the bound columns from truncating, so it should not occur for these columns in practice.
1636 if (!SQL_SUCCEEDED(fetchResult))
1637 RequireSuccess(fetchResult);
1638
1639 auto const fetched = static_cast<std::size_t>(rowsFetched);
1640 SqlLogger::GetLogger().OnFetchRow(); // one block-fetch round-trip (vs. one per row on the slow path)
1641
1642 std::size_t finalizeIndex = 0;
1643 (FinalizeRowWiseOutputColumn<std::remove_cvref_t<decltype(accessors(*row0))>>(
1644 std::addressof(accessors(*row0)), sizeof(Record), fetched, indicators[finalizeIndex++]),
1645 ...);
1646
1647 out.resize(base + fetched);
1648 if (fetched < arrayDepth)
1649 break;
1650 }
1651
1652 SqlLogger::GetLogger().OnFetchEnd();
1653}
1654
1655template <SqlGetColumnNativeType T>
1656inline bool SqlStatement::GetColumn(SQLUSMALLINT column, T* result) const
1657{
1658 if (IsPrefetchActive())
1659 {
1660 auto const& cursor = PrefetchCursorRef();
1661 auto const row = PrefetchRowInBlock();
1662 RequirePrefetchColumnInRange(cursor, column);
1663 if (cursor.IsCellNull(row, column))
1664 return false;
1665 *result = ConvertCell<T>(cursor, row, column);
1666 return true;
1667 }
1668 SQLLEN indicator {}; // TODO: Handle NULL values if we find out that we need them for our use-cases.
1669 RequireSuccess(SqlDataBinder<T>::GetColumn(m_hStmt, column, result, &indicator, *this));
1670 return indicator != SQL_NULL_DATA;
1671}
1672
1673namespace detail
1674{
1675
1676 template <typename T>
1677 concept SqlNullableType = (std::same_as<T, SqlVariant> || IsSpecializationOf<std::optional, T>);
1678
1679 /// Detects @c SqlFixedString<N, Char, Mode> specializations (the inline fixed-capacity strings).
1680 template <typename T>
1681 struct IsSqlFixedStringSpec: std::false_type
1682 {
1683 };
1684 template <std::size_t N, typename Char, SqlFixedStringMode Mode>
1685 struct IsSqlFixedStringSpec<SqlFixedString<N, Char, Mode>>: std::true_type
1686 {
1687 };
1688 template <typename T>
1689 concept SqlFixedStringCell = IsSqlFixedStringSpec<std::remove_cvref_t<T>>::value;
1690
1691 /// The plain standard string flavours the block-prefetch reader converts to from UTF-8 bytes.
1692 template <typename T>
1693 concept PlainStringCell =
1694 std::same_as<T, std::string> || std::same_as<T, std::u8string> || std::same_as<T, std::u16string>
1695 || std::same_as<T, std::u32string> || std::same_as<T, std::wstring>;
1696
1697 /// Detects @c SqlNumeric<Precision, Scale> specializations.
1698 template <typename T>
1699 struct IsSqlNumericSpec: std::false_type
1700 {
1701 };
1702 template <std::size_t Precision, std::size_t Scale>
1703 struct IsSqlNumericSpec<SqlNumeric<Precision, Scale>>: std::true_type
1704 {
1705 };
1706 template <typename T>
1707 concept SqlNumericCell = IsSqlNumericSpec<std::remove_cvref_t<T>>::value;
1708
1709 /// Views a UTF-8 @c std::string (opaque byte container) as a @c std::u8string_view for conversion.
1710 [[nodiscard]] inline std::u8string_view AsU8View(std::string const& utf8) noexcept
1711 {
1712 return std::u8string_view { reinterpret_cast<char8_t const*>(utf8.data()), utf8.size() };
1713 }
1714
1715 /// @brief Trims the trailing bytes of a fetched fixed-string value to match
1716 /// @c SqlFixedString::PostProcessOutputColumn (which the single-row @c GetColumn path applies), so a
1717 /// prefetched value is byte-identical to a per-row read. Every mode strips trailing NULs;
1718 /// @c FIXED_SIZE_RIGHT_TRIMMED additionally strips trailing ASCII whitespace (e.g. @c CHAR(N) space
1719 /// padding). Operates on the raw UTF-8 bytes before any wide conversion — ASCII whitespace/NUL are
1720 /// single bytes that map one-to-one to their wide code units, so the result matches a trim applied
1721 /// after conversion.
1722 /// @tparam Mode The fixed string's @c SqlFixedStringMode (its @c PostRetrieveOperation).
1723 /// @param bytes The fetched UTF-8 bytes, trimmed in place.
1724 template <SqlFixedStringMode Mode>
1725 inline void TrimFixedStringBytes(std::string& bytes) noexcept
1726 {
1727 auto const isTrailingTrimmable = [](char c) noexcept {
1728 if (c == '\0')
1729 return true;
1730 if constexpr (Mode == SqlFixedStringMode::FIXED_SIZE_RIGHT_TRIMMED)
1731 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
1732 else
1733 return false;
1734 };
1735 while (!bytes.empty() && isTrailingTrimmable(bytes.back()))
1736 bytes.pop_back();
1737 }
1738
1739 /// @brief Decodes the fetched UTF-8 bytes into a @c std::basic_string of the target character type
1740 /// @p Char, reusing the project's UnicodeConverter. The block-prefetch reader stores text as UTF-8
1741 /// (RowArrayCursor::GetString); this re-encodes it to the string target's element type.
1742 /// @tparam Char The target character type (@c char / @c char8_t / @c char16_t / @c char32_t / @c wchar_t).
1743 /// @param utf8 The fetched UTF-8 bytes.
1744 /// @return The decoded string in the target encoding.
1745 template <typename Char>
1746 [[nodiscard]] inline std::basic_string<Char> DecodeUtf8To(std::string const& utf8)
1747 {
1748 if constexpr (std::same_as<Char, char>)
1749 return utf8;
1750 else if constexpr (std::same_as<Char, char8_t>)
1751 return std::u8string { AsU8View(utf8) };
1752 else if constexpr (std::same_as<Char, char16_t>)
1753 return ToUtf16(AsU8View(utf8));
1754 else if constexpr (std::same_as<Char, char32_t>)
1755 return ToUtf32<std::u32string>(AsU8View(utf8));
1756 else
1757 return ToStdWideString(AsU8View(utf8));
1758 }
1759
1760 /// @brief Any string-like target the block-prefetch reader reconstructs from UTF-8 bytes: the plain
1761 /// standard strings plus the Lightweight string wrappers (fixed- and dynamic-capacity). Each exposes a
1762 /// @c value_type and is constructible from a @c std::basic_string of that type.
1763 template <typename T>
1764 concept StringLikeCell = PlainStringCell<T> || SqlStringInterface<T>;
1765
1766 /// A scalar target type the block-prefetch reader can reconstruct faithfully (mirrors the non-throwing
1767 /// branches of @c SqlStatement::ConvertCell). Excludes types whose faithful reconstruction needs the
1768 /// dedicated single-row binder (e.g. @c SqlNumeric, @c SqlTime, binary, user types).
1769 template <typename T>
1770 concept PrefetchConvertibleScalar =
1771 std::same_as<T, SqlVariant> || std::same_as<T, SqlDate> || std::same_as<T, SqlDateTime> || std::same_as<T, SqlGuid>
1772 || StringLikeCell<T> || std::is_floating_point_v<T> || std::is_integral_v<T> || std::is_enum_v<T>;
1773
1774 template <typename T>
1775 struct PrefetchConvertibleOptional: std::false_type
1776 {
1777 };
1778 template <typename U>
1779 struct PrefetchConvertibleOptional<std::optional<U>>: std::bool_constant<PrefetchConvertibleScalar<U>>
1780 {
1781 };
1782
1783 /// A bound output target the prefetch scatter can serve: a convertible scalar or an optional of one.
1784 template <typename T>
1785 concept PrefetchConvertible = PrefetchConvertibleScalar<T> || PrefetchConvertibleOptional<T>::value;
1786
1787 /// @brief Reconstructs a temporal or GUID cell from the block buffer. Each target reads its matching
1788 /// bound representation; a mismatched bound type (only reachable via a cross-type @c GetColumn) yields
1789 /// a default, mirroring the lenient single-row path. A GUID stored as text (SQLite) is parsed back.
1790 template <typename T>
1791 [[nodiscard]] inline T ReadTemporalGuidCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column)
1792 {
1793 using BoundType = RowArrayCursor::BoundType;
1794 auto const boundType = cursor.ColumnBoundType(column);
1795 if constexpr (std::same_as<T, SqlDate>)
1796 return boundType == BoundType::Date ? cursor.GetDate(row, column).value_or(SqlDate {}) : SqlDate {};
1797 else if constexpr (std::same_as<T, SqlDateTime>)
1798 return boundType == BoundType::Timestamp ? cursor.GetTimestamp(row, column).value_or(SqlDateTime {})
1799 : SqlDateTime {};
1800 else // SqlGuid
1801 {
1802 if (boundType == BoundType::Guid)
1803 return cursor.GetGuid(row, column).value_or(SqlGuid {});
1804 if (boundType == BoundType::Char || boundType == BoundType::WChar)
1805 return SqlGuid::TryParse(cursor.GetString(row, column).value_or(std::string {})).value_or(SqlGuid {});
1806 return SqlGuid {};
1807 }
1808 }
1809
1810 /// @brief Reconstructs a @c SqlNumeric cell from the block buffer (driver-reported as a fixed-width
1811 /// numeric, bound @c Int64 or @c Double). A non-numeric bound type yields a default.
1812 template <typename T>
1813 [[nodiscard]] inline T ReadNumericCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column)
1814 {
1815 using BoundType = RowArrayCursor::BoundType;
1816 switch (cursor.ColumnBoundType(column))
1817 {
1818 case BoundType::Double:
1819 return T { cursor.GetF64(row, column).value_or(0.0) };
1820 case BoundType::Int64:
1821 return T { static_cast<double>(cursor.GetI64(row, column).value_or(0)) };
1822 default:
1823 return T {};
1824 }
1825 }
1826
1827 /// @brief Renders a block-buffer cell to UTF-8 text. Character columns are returned verbatim;
1828 /// numeric, temporal and GUID columns are formatted to their text form. This mirrors the driver's
1829 /// @c SQL_C_CHAR conversion on the single-row @c GetColumn path so that reading a non-character column
1830 /// as a string (e.g. a generic "print every column as text" loop) yields the value rather than an
1831 /// empty string. Integer text is identical to the driver's; floating/temporal text uses the value
1832 /// type's @c std::formatter, which is backend-independent.
1833 [[nodiscard]] inline std::string RenderCellAsUtf8(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column)
1834 {
1835 switch (cursor.ColumnBoundType(column))
1836 {
1837 case RowArrayCursor::BoundType::Char:
1838 case RowArrayCursor::BoundType::WChar:
1839 return cursor.GetString(row, column).value_or(std::string {});
1840 case RowArrayCursor::BoundType::Int64:
1841 return std::format("{}", cursor.GetI64(row, column).value_or(0));
1842 case RowArrayCursor::BoundType::Double:
1843 return std::format("{}", cursor.GetF64(row, column).value_or(0.0));
1844 case RowArrayCursor::BoundType::Date:
1845 return std::format("{}", cursor.GetDate(row, column).value_or(SqlDate {}));
1846 case RowArrayCursor::BoundType::Timestamp:
1847 return std::format("{}", cursor.GetTimestamp(row, column).value_or(SqlDateTime {}));
1848 case RowArrayCursor::BoundType::Guid:
1849 return std::format("{}", cursor.GetGuid(row, column).value_or(SqlGuid {}));
1850 }
1851 return std::string {};
1852 }
1853
1854 /// @brief Reconstructs a string-like cell (plain @c std::string flavours and the Lightweight string
1855 /// wrappers) from the block buffer, rendering any bound type to text via @ref RenderCellAsUtf8.
1856 /// Fixed-capacity strings get the same trailing trim the single-row @c GetColumn path applies via
1857 /// @c SqlFixedString::PostProcessOutputColumn; the UTF-8 bytes are then re-encoded to the target's
1858 /// element type.
1859 template <typename T>
1860 [[nodiscard]] inline T ReadStringLikeCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column)
1861 {
1862 auto utf8 = RenderCellAsUtf8(cursor, row, column);
1863 if constexpr (SqlFixedStringCell<T>)
1864 TrimFixedStringBytes<T::PostRetrieveOperation>(utf8);
1865 return T { DecodeUtf8To<typename T::value_type>(utf8) };
1866 }
1867
1868 /// @brief Reconstructs an arithmetic or enum cell from the block buffer, coercing whichever fixed-width
1869 /// representation the column was bound as (@c Int64 or @c Double) to @p T. A non-arithmetic bound type
1870 /// yields a default.
1871 template <typename T>
1872 [[nodiscard]] inline T ReadArithmeticCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column)
1873 {
1874 using BoundType = RowArrayCursor::BoundType;
1875 switch (cursor.ColumnBoundType(column))
1876 {
1877 case BoundType::Int64:
1878 return static_cast<T>(cursor.GetI64(row, column).value_or(0));
1879 case BoundType::Double:
1880 return static_cast<T>(cursor.GetF64(row, column).value_or(0.0));
1881 default:
1882 return T {};
1883 }
1884 }
1885
1886} // end namespace detail
1887
1888template <typename T>
1889inline T SqlStatement::ConvertCell(RowArrayCursor const& cursor, std::size_t row, SQLUSMALLINT column) const
1890{
1891 // Dispatch the target type to the matching reconstruction helper. The arming allowlist keeps the
1892 // column's bound representation in step with the natural target type; each helper additionally guards
1893 // on the bound type so a cross-type raw GetColumn read degrades to a default rather than throwing.
1894 if constexpr (std::same_as<T, SqlVariant>)
1895 return MakePrefetchVariantCell(cursor, row, column);
1896 else if constexpr (IsSpecializationOf<std::optional, T>)
1897 {
1898 if (cursor.IsCellNull(row, column))
1899 return std::nullopt;
1900 return T { ConvertCell<typename T::value_type>(cursor, row, column) };
1901 }
1902 else if constexpr (std::same_as<T, SqlDate> || std::same_as<T, SqlDateTime> || std::same_as<T, SqlGuid>)
1903 return detail::ReadTemporalGuidCell<T>(cursor, row, column);
1904 else if constexpr (detail::SqlNumericCell<T>)
1905 return detail::ReadNumericCell<T>(cursor, row, column);
1906 else if constexpr (detail::StringLikeCell<T>)
1907 return detail::ReadStringLikeCell<T>(cursor, row, column);
1908 else if constexpr (std::is_floating_point_v<T> || std::is_integral_v<T> || std::is_enum_v<T>)
1909 return detail::ReadArithmeticCell<T>(cursor, row, column);
1910 else
1911 // A target type the block buffer cannot reconstruct (e.g. a user type with a custom binder). The
1912 // bound path declines prefetch for such targets (see PrefetchConvertible); reaching here via a raw
1913 // GetColumn returns a default rather than crashing.
1914 return T {};
1915}
1916
1917template <SqlOutputColumnBinder T>
1918inline void SqlStatement::RecordPrefetchOutputColumn(SQLUSMALLINT column, T* arg)
1919{
1920 auto deferredBind = [this, column, arg] {
1921 RequireIndicators();
1922 RequireSuccess(SqlDataBinder<T>::OutputColumn(m_hStmt, column, arg, GetIndicatorForColumn(column), *this));
1923 };
1924 if constexpr (detail::PrefetchConvertible<T>)
1925 {
1926 RecordPrefetchColumn(
1927 column,
1928 [this, column, arg] { *arg = ConvertCell<T>(PrefetchCursorRef(), PrefetchRowInBlock(), column); },
1929 std::move(deferredBind));
1930 }
1931 else
1932 {
1933 // The target type cannot be reconstructed from the block buffer; record only the real bind and
1934 // flag the set so arming declines prefetch and the deferred binds drive the per-row path.
1935 RecordPrefetchColumn(column, {}, std::move(deferredBind));
1936 MarkPrefetchBindingUnsupported();
1937 }
1938}
1939
1940template <SqlGetColumnNativeType T>
1941inline T SqlStatement::GetColumn(SQLUSMALLINT column) const
1942{
1943 if (IsPrefetchActive())
1944 {
1945 auto const& cursor = PrefetchCursorRef();
1946 auto const row = PrefetchRowInBlock();
1947 RequirePrefetchColumnInRange(cursor, column);
1948 if constexpr (!detail::SqlNullableType<T>)
1949 if (cursor.IsCellNull(row, column))
1950 throw std::runtime_error { "Column value is NULL" };
1951 return ConvertCell<T>(cursor, row, column);
1952 }
1953 T result {};
1954 SQLLEN indicator {};
1955 {
1956 // SQLGetData is where the ODBC driver materializes the column value (driver/network I/O).
1957 // Isolating it lets a profiler separate I/O-bound retrieval from CPU-bound value conversion
1958 // done by the caller — the key question for deciding what to parallelize.
1959 ZoneScopedN("SqlStatement::ColumnGetData");
1960 RequireSuccess(SqlDataBinder<T>::GetColumn(m_hStmt, column, &result, &indicator, *this));
1961 }
1962 if constexpr (!detail::SqlNullableType<T>)
1963 if (indicator == SQL_NULL_DATA)
1964 throw std::runtime_error { "Column value is NULL" };
1965 return result;
1966}
1967
1968template <SqlGetColumnNativeType T>
1969inline std::optional<T> SqlStatement::GetNullableColumn(SQLUSMALLINT column) const
1970{
1971 if (IsPrefetchActive())
1972 {
1973 auto const& cursor = PrefetchCursorRef();
1974 auto const row = PrefetchRowInBlock();
1975 RequirePrefetchColumnInRange(cursor, column);
1976 if (cursor.IsCellNull(row, column))
1977 return std::nullopt;
1978 return ConvertCell<T>(cursor, row, column);
1979 }
1980 T result {};
1981 SQLLEN indicator {}; // TODO: Handle NULL values if we find out that we need them for our use-cases.
1982 {
1983 ZoneScopedN("SqlStatement::ColumnGetData");
1984 RequireSuccess(SqlDataBinder<T>::GetColumn(m_hStmt, column, &result, &indicator, *this));
1985 }
1986 if (indicator == SQL_NULL_DATA)
1987 return std::nullopt;
1988 return { std::move(result) };
1989}
1990
1991template <SqlGetColumnNativeType T>
1992T SqlStatement::GetColumnOr(SQLUSMALLINT column, T&& defaultValue) const
1993{
1994 return GetNullableColumn<T>(column).value_or(std::forward<T>(defaultValue));
1995}
1996
1997inline LIGHTWEIGHT_FORCE_INLINE SqlResultCursor SqlStatement::ExecuteDirect(SqlQueryObject auto const& query,
1998 std::source_location location)
1999{
2000 return ExecuteDirect(query.ToSql(), location);
2001}
2002
2003template <typename Callable>
2004 requires std::invocable<Callable, SqlMigrationQueryBuilder&>
2005void SqlStatement::MigrateDirect(Callable const& callable, std::source_location location)
2006{
2007 ZoneScopedN("SqlStatement::MigrateDirect");
2008 auto migration = SqlMigrationQueryBuilder { Connection().QueryFormatter() };
2009 callable(migration);
2010 auto const queries = migration.GetPlan().ToSql();
2011 ZoneValue(queries.size());
2012
2013 // A comment-only `-- LIGHTWEIGHT_SQLITE_GUARD:` script (e.g. ALTER COLUMN or a foreign-key change on
2014 // SQLite) carries no executable DDL: the schema change is performed by the migration executor's
2015 // table-rebuild path, which only runs via MigrationManager. Executing such a script directly here
2016 // would silently do nothing, so fail loudly and point at the supported entry point instead.
2017 auto const isCommentOnlyGuardScript = [](std::string_view script) {
2018 constexpr std::string_view marker = "-- LIGHTWEIGHT_SQLITE_GUARD:";
2019 if (!script.starts_with(marker))
2020 return false;
2021 auto const newline = script.find('\n');
2022 if (newline == std::string_view::npos)
2023 return true; // sentinel line only, nothing executable follows
2024 auto const body = script.substr(newline + 1);
2025 auto const bodyStart = body.find_first_not_of(" \t\r\n");
2026 return bodyStart == std::string_view::npos || body.substr(bodyStart).starts_with("--");
2027 };
2028
2029 for (auto const& query: queries)
2030 {
2031 if (isCommentOnlyGuardScript(query))
2032 throw std::runtime_error(
2033 std::format("SqlStatement::MigrateDirect cannot apply this SQLite schema change directly because it "
2034 "requires a table rebuild (e.g. ALTER COLUMN or a foreign-key change). Apply it through "
2035 "MigrationManager::ApplyPendingMigrations, which runs the rebuild executor.\n Script: {}",
2036 query));
2037 [[maybe_unused]] auto cursor = ExecuteDirect(query, location);
2038 }
2039}
2040
2041template <typename T>
2042 requires(!std::same_as<T, SqlVariant>)
2043inline std::optional<T> SqlStatement::ExecuteDirectScalar(std::string_view const& query, std::source_location location)
2044{
2045 auto cursor = ExecuteDirect(query, location);
2046 RequireSuccess(FetchRow());
2047 return GetNullableColumn<T>(1);
2048}
2049
2050template <typename T>
2051 requires(std::same_as<T, SqlVariant>)
2052inline T SqlStatement::ExecuteDirectScalar(std::string_view const& query, std::source_location location)
2053{
2054 auto cursor = ExecuteDirect(query, location);
2055 RequireSuccess(FetchRow());
2056 if (auto result = GetNullableColumn<T>(1); result.has_value())
2057 return *result;
2058 return SqlVariant { SqlNullValue };
2059}
2060
2061template <typename T>
2062 requires(!std::same_as<T, SqlVariant>)
2063inline std::optional<T> SqlStatement::ExecuteDirectScalar(SqlQueryObject auto const& query, std::source_location location)
2064{
2065 return ExecuteDirectScalar<T>(query.ToSql(), location);
2066}
2067
2068template <typename T>
2069 requires(std::same_as<T, SqlVariant>)
2070inline T SqlStatement::ExecuteDirectScalar(SqlQueryObject auto const& query, std::source_location location)
2071{
2072 return ExecuteDirectScalar<T>(query.ToSql(), location);
2073}
2074
2075inline LIGHTWEIGHT_FORCE_INLINE void SqlStatement::CloseCursor() noexcept
2076{
2077 // Tear down any block-prefetch first: the RowArrayCursor destructor unbinds the columns and
2078 // restores SQL_ATTR_ROW_ARRAY_SIZE so the SQLFreeStmt(SQL_CLOSE) below — and the next query on this
2079 // statement — start from a clean single-row state. Resets the prefetch lifecycle to Unarmed.
2080 ResetPrefetchState();
2081
2082 // SQL Server batches and DML/DDL row-count tokens produce multiple result
2083 // sets per SQLExecDirect. SQLFreeStmt(SQL_CLOSE) only discards the current
2084 // cursor — remaining result sets stay pending on the *connection*, and
2085 // without MARS every subsequent statement on that connection fails with
2086 // HY000 "Connection is busy with results for another command". Drain via
2087 // SQLMoreResults until SQL_NO_DATA (or an error), then close.
2088 //
2089 // SQLMoreResults is standard ODBC; SQLite and PostgreSQL drivers return
2090 // SQL_NO_DATA on the first call when nothing is pending, so the cost on
2091 // single-statement queries is one no-op driver call.
2092 while (true)
2093 {
2094 auto const rc = SQLMoreResults(m_hStmt);
2095 if (rc == SQL_NO_DATA || !SQL_SUCCEEDED(rc))
2096 break;
2097 }
2098 SQLFreeStmt(m_hStmt, SQL_CLOSE);
2099 SqlLogger::GetLogger().OnFetchEnd();
2100}
2101
2102// }}}
2103
2104} // namespace Lightweight
Thrown by RowArrayCursor's constructor when the executed result set cannot be fixed-stride array-boun...
A cursor that fetches result rows in bulk (ODBC row-array binding) for fast column reads.
LIGHTWEIGHT_API SQLSMALLINT ColumnSqlType(SQLUSMALLINT column) const
The raw SQL data type the driver reported for a result column (the SQL_* value from SQLDescribeCol),...
LIGHTWEIGHT_API RowArrayCursor(SqlStatement &stmt, std::size_t arrayDepth)
Constructs the cursor on a statement whose query has already been executed. Inspects the result colum...
LIGHTWEIGHT_API BoundType ColumnBoundType(SQLUSMALLINT column) const
The bound representation chosen for a result column.
LIGHTWEIGHT_API ~RowArrayCursor() noexcept
Resets the statement's row-array attributes and unbinds the columns so the handle can be safely reuse...
LIGHTWEIGHT_API bool IsCellNull(std::size_t rowInBatch, SQLUSMALLINT column) const
Whether a cell in the last fetched block is SQL NULL.
BoundType
How a result column is bound for bulk fetch (the canonical fixed-stride C representation chosen from ...
Represents a connection to a SQL database.
LIGHTWEIGHT_API bool IsAlive() const noexcept
Tests if the connection is still active.
Query builder for building SQL migration queries.
Definition Migrate.hpp:477
API Entry point for building SQL queries.
Definition SqlQuery.hpp:32
API for reading an SQL query result set.
LIGHTWEIGHT_FORCE_INLINE void BindOutputColumnsToRecord(Records *... records)
Binds the given records to the prepared statement to store the fetched data to.
constexpr SqlResultCursor(SqlResultCursor &&other) noexcept
Move constructor.
LIGHTWEIGHT_FORCE_INLINE SqlResultCursor(SqlStatement &stmt) noexcept
Constructs a result cursor for the given SQL statement.
LIGHTWEIGHT_FORCE_INLINE void FetchAllRowWise(std::vector< Record > &out, std::size_t arrayDepth, ColumnAccessors const &... accessors)
Fast bulk retrieval: materializes this result set into out via native ODBC row-wise array fetch....
constexpr SqlResultCursor & operator=(SqlResultCursor &&other) noexcept
Move assignment operator.
LIGHTWEIGHT_FORCE_INLINE bool GetColumn(SQLUSMALLINT column, T *result) const
LIGHTWEIGHT_FORCE_INLINE void BindOutputColumns(Args *... args)
LIGHTWEIGHT_FORCE_INLINE std::optional< T > GetNullableColumn(SQLUSMALLINT column) const
LIGHTWEIGHT_FORCE_INLINE T GetColumn(SQLUSMALLINT column) const
Retrieves the value of the column at the given index for the currently selected row.
T GetColumnOr(SQLUSMALLINT column, T &&defaultValue) 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.
LIGHTWEIGHT_FORCE_INLINE std::expected< bool, SqlErrorInfo > TryFetchRow(std::source_location location=std::source_location::current()) noexcept
Attempts to fetch the next row, returning an error info on failure instead of throwing.
SQL query result row iterator.
SqlRowIterator(SqlConnection &conn)
Constructs a row iterator using the given SQL connection.
iterator end() noexcept
Returns a sentinel iterator representing the end of the result set.
iterator begin()
Returns an iterator to the first row of the result set.
High level API for (prepared) raw SQL statements.
LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const &table, std::string_view const &tableAlias) const
Creates a new query builder for the given table with an alias, compatible with the SQL server being c...
LIGHTWEIGHT_API SqlStatement(SqlStatement &&other) noexcept
Move constructor.
LIGHTWEIGHT_API SqlStatement()
Construct a new SqlStatement object, using a new connection, and connect to the default database.
LIGHTWEIGHT_API SqlStatement & operator=(SqlStatement &&other) noexcept
Move assignment operator.
LIGHTWEIGHT_API SqlStatement(std::nullopt_t)
Construct a new empty SqlStatement object. No SqlConnection is associated with this statement.
LIGHTWEIGHT_API SqlStatement(SqlConnection &relatedConnection)
Construct a new SqlStatement object, using the given connection.
Requires that T maps onto a column of its record's table.
Definition Record.hpp:302
Whether V's binder provides a row-wise batch entry point (BatchRowWiseInputParameter).
A value type that can be bound in a native ODBC row-wise parameter array (fixed-width,...
A std::optional column that can be bound zero-copy in a native row-wise batch: the contained type is ...
Represents an SQL query object, that provides a ToSql() method.
A column value type usable on the native row-wise batch path — either a row-bindable fixed value or a...
A column usable on the native row-wise array-FETCH fast path. Intentionally identical to the write-si...
constexpr void EnumerateRecordMembers(Record &record, Callable &&callable)
Invokes callable as callable<I>(member) for each member of record.
constexpr auto SqlNullValue
std::u16string ToUtf16(std::basic_string_view< T > const u32InputString)
LIGHTWEIGHT_API std::wstring ToStdWideString(std::u8string_view u8InputString)
Represents an ODBC SQL error.
Definition SqlError.hpp:33
A non-owning reference to a raw column data for batch processing.
Represents a value that can be any of the supported SQL data types.