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