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