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