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