Lightweight 0.20260625.0
Loading...
Searching...
No Matches
SqlConnection.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5// See SqlOdbcPrelude.hpp's header comment for why this replaces a direct <Windows.h> include.
6#include "Api.hpp"
7#include "Async/Fwd.hpp"
8#include "SqlConnectInfo.hpp"
9#include "SqlError.hpp"
10#include "SqlLogger.hpp"
11#include "SqlOdbcPrelude.hpp"
12#include "SqlServerType.hpp"
13
14#include <atomic>
15#include <chrono>
16#include <expected>
17#include <format>
18#include <functional>
19#include <memory>
20#include <optional>
21#include <string>
22#include <string_view>
23#include <system_error>
24
25#include <sql.h>
26#include <sqlext.h>
27#include <sqlspi.h>
28#include <sqltypes.h>
29
30/// @defgroup CoreApi Core API
31/// @brief The low-level SQL API: connections, statements, result cursors and transactions.
32///
33/// This is the thin layer directly over ODBC. Use it when you want to write SQL yourself and
34/// keep full control over binding and fetching. The higher-level @ref DataMapper and
35/// @ref QueryBuilder layers are built on top of these types and interoperate with them.
36
37namespace Lightweight
38{
39
40class SqlQueryBuilder;
41class SqlMigrationQueryBuilder;
42class SqlQueryFormatter;
43
44/// @ingroup CoreApi
45/// @brief Represents a connection to a SQL database.
46class SqlConnection final
47{
48 public:
49 /// @brief Constructs a new SQL connection to the default connection.
50 ///
51 /// The default connection is set via SetDefaultConnectInfo.
52 /// In case the default connection is not set, the connection will fail.
53 /// And in case the connection fails, the last error will be set.
54 LIGHTWEIGHT_API SqlConnection();
55
56 /// @brief Constructs a new SQL connection to the given connect informaton.
57 ///
58 /// @param connectInfo The connection information to use. If `std::nullopt`,
59 /// no connection is established and the object is left
60 /// in an unconnected state — use `Connect(...)` later.
61 /// If a value is provided and the connection fails,
62 /// `SqlException` is thrown carrying the ODBC diagnostic
63 /// read from the DBC handle.
64 LIGHTWEIGHT_API explicit SqlConnection(std::optional<SqlConnectionString> connectInfo);
65
66 /// Move constructor.
67 LIGHTWEIGHT_API SqlConnection(SqlConnection&& /*other*/) noexcept;
68 /// Move assignment operator.
69 LIGHTWEIGHT_API SqlConnection& operator=(SqlConnection&& /*other*/) noexcept;
70 SqlConnection(SqlConnection const& /*other*/) = delete;
71 SqlConnection& operator=(SqlConnection const& /*other*/) = delete;
72
73 /// Destructs this SQL connection object,
74 LIGHTWEIGHT_API ~SqlConnection() noexcept;
75
76 /// Retrieves the default connection information.
77 LIGHTWEIGHT_API static SqlConnectionString const& DefaultConnectionString() noexcept;
78
79 /// Sets the default connection information.
80 ///
81 /// @param connectionString The connection information to use.
82 LIGHTWEIGHT_API static void SetDefaultConnectionString(SqlConnectionString const& connectionString) noexcept;
83
84 /// Sets the default connection information as SqlConnectionDataSource.
85 LIGHTWEIGHT_API static void SetDefaultDataSource(SqlConnectionDataSource const& dataSource) noexcept;
86
87 /// Sets a callback to be called after each connection being established.
88 LIGHTWEIGHT_API static void SetPostConnectedHook(std::function<void(SqlConnection&)> hook);
89
90 /// Resets the post connected hook.
91 LIGHTWEIGHT_API static void ResetPostConnectedHook();
92
93 /// @brief Retrieves the connection ID.
94 ///
95 /// This is a unique identifier for the connection, which is useful for debugging purposes.
96 /// Note, this ID will not change if the connection is moved nor when it is reused via the connection pool.
97 [[nodiscard]] uint64_t ConnectionId() const noexcept
98 {
99 return m_connectionId;
100 }
101
102 /// Closes the connection, freeing the underlying ODBC handles.
103 ///
104 /// This does not return the connection to any pool: `SqlConnection` owns its handles outright.
105 /// Connection pooling is provided separately by `Lightweight::Pool` in
106 /// `DataMapper/Pool.hpp`, which recycles pooled `DataMapper` instances.
107 LIGHTWEIGHT_API void Close() noexcept;
108
109 /// Connects to the given database with the given username and password.
110 ///
111 /// This method can be called on a connection that has been closed via Close().
112 /// If the ODBC handles have been freed, they will be automatically reallocated.
113 ///
114 /// @retval true if the connection was successful.
115 /// @retval false if the connection failed. Use LastError() to retrieve the error information.
116 LIGHTWEIGHT_API bool Connect(SqlConnectionDataSource const& info) noexcept;
117
118 /// Connects to the given database with the given connection string.
119 ///
120 /// This method can be called on a connection that has been closed via Close().
121 /// If the ODBC handles have been freed, they will be automatically reallocated.
122 ///
123 /// @retval true if the connection was successful.
124 /// @retval false if the connection failed. Use LastError() to retrieve the error information.
125 LIGHTWEIGHT_API bool Connect(SqlConnectionString sqlConnectionString) noexcept;
126
127 /// Retrieves the last error information with respect to this SQL connection handle.
128 [[nodiscard]] LIGHTWEIGHT_API SqlErrorInfo LastError() const;
129
130 /// Retrieves the name of the database in use.
131 [[nodiscard]] LIGHTWEIGHT_API std::string DatabaseName() const;
132
133 /// Retrieves the name of the user.
134 [[nodiscard]] LIGHTWEIGHT_API std::string UserName() const;
135
136 /// Retrieves the name of the server.
137 [[nodiscard]] LIGHTWEIGHT_API std::string ServerName() const;
138
139 /// Retrieves the reported server version.
140 [[nodiscard]] LIGHTWEIGHT_API std::string ServerVersion() const;
141
142 /// Retrieves the type of the server.
143 [[nodiscard]] SqlServerType ServerType() const noexcept;
144
145 /// Retrieves the name of the driver used for this connection.
146 [[nodiscard]] std::string const& DriverName() const noexcept;
147
148 /// Retrieves a query formatter suitable for the SQL server being connected.
149 [[nodiscard]] SqlQueryFormatter const& QueryFormatter() const noexcept;
150
151 /// @brief Whether this backend must rebuild a table to apply an `ALTER TABLE` schema change it
152 /// cannot express in place (foreign-key add/drop, or column type/nullability change).
153 ///
154 /// The migration executor consults this to decide whether a `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinel
155 /// must be turned into a table rebuild (`true` for SQLite) or executed directly. Exposed on the
156 /// connection so capability decisions go through the backend, delegating the dialect knowledge to
157 /// the query formatter.
158 [[nodiscard]] LIGHTWEIGHT_API bool RequiresTableRebuildForSchemaChange() const noexcept;
159
160 /// @brief Whether this connection's ODBC driver supports native parameter-array binding
161 /// (`SQL_ATTR_PARAMSET_SIZE` > 1) for batched row-wise execution.
162 ///
163 /// The batched `SqlStatement::ExecuteBatch(rows, accessors...)` consults this to decide whether it
164 /// may submit the whole batch in a single row-wise `SQLExecute`, or must fall back to a single
165 /// prepare followed by consecutive per-row executes. This is a driver/backend capability — not a
166 /// SQL-dialect concern — so it belongs to the connection, which knows both the server type and the
167 /// driver name (either of which a future carve-out can branch on).
168 ///
169 /// @return `true` if the driver honours parameter arrays.
170 [[nodiscard]] bool SupportsNativeRowBatch() const noexcept;
171
172 /// @brief Whether this connection's ODBC driver supports native row-array fetching
173 /// (`SQL_ATTR_ROW_ARRAY_SIZE` > 1 with `SQLFetchScroll`) for block result retrieval.
174 ///
175 /// The fast retrieval path in @c SqlStatement::FetchAllRowWise consults this to decide whether
176 /// it may bind the result columns row-wise over a record block and materialize whole row blocks per
177 /// `SQLFetchScroll` round-trip, or must fall back to the per-row `SQLFetch` path. Like
178 /// @ref SupportsNativeRowBatch this is a driver/backend capability — not a SQL-dialect concern — so
179 /// it lives on the connection. Kept distinct from the parameter-array flag so a backend that honours
180 /// one but not the other can be carved out independently.
181 ///
182 /// @return `true` if the driver honours row-array fetching.
183 [[nodiscard]] bool SupportsNativeRowArrayFetch() const noexcept;
184
185 /// @brief Server-type overload of @ref SupportsNativeRowArrayFetch, for callers that hold only the
186 /// server type (e.g. the DataMapper result reader) and not the connection. Keeps the single source of
187 /// truth for this capability on the connection rather than scattering a `switch (serverType)` into
188 /// business logic.
189 /// @param serverType The backend server type to test.
190 /// @return `true` if that backend honours row-array fetching.
191 [[nodiscard]] static bool SupportsNativeRowArrayFetch(SqlServerType serverType) noexcept;
192
193 /// @brief Whether @p serverType's driver round-trips narrow (@c SQL_C_CHAR) character data
194 /// byte-exact, so a fixed-capacity char string may be array-bound narrow on the row-wise fetch path.
195 ///
196 /// PostgreSQL's psqlODBC transcodes @c SQL_C_CHAR through the client codepage (cp1252 on Windows),
197 /// mangling non-ASCII bytes — its single-row binder therefore reads narrow strings via @c SQL_C_WCHAR.
198 /// That wide round-trip needs an external per-cell buffer + conversion, which cannot be expressed as an
199 /// in-place row-wise array bind, so a record carrying a fixed-capacity string falls back to the per-row
200 /// path on PostgreSQL. MS SQL Server and SQLite read @c SQL_C_CHAR verbatim and stay on the fast path.
201 ///
202 /// @param serverType The backend server type to test.
203 /// @return `true` if narrow character data round-trips byte-exact on that backend.
204 [[nodiscard]] static bool RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept;
205
206 /// @brief The default block-prefetch depth applied to statements created on this connection.
207 ///
208 /// Classic per-row fetch loops (`while (cursor.FetchRow()) ...`, @c SqlRowIterator,
209 /// @c SqlVariantRowCursor) transparently request this many rows per @c SQLFetchScroll round-trip
210 /// instead of issuing one @c SQLFetch per row. A value <= 1 disables prefetch. Effective only when
211 /// @ref SupportsNativeRowArrayFetch is true; otherwise statements fall back to per-row fetching.
212 ///
213 /// @return The configured default prefetch depth (defaults to @c PrefetchDepthDefault).
214 [[nodiscard]] LIGHTWEIGHT_API std::size_t DefaultPrefetchDepth() const noexcept;
215
216 /// @brief Sets the default block-prefetch depth for statements created on this connection.
217 ///
218 /// @param depth Rows to request per @c SQLFetchScroll round-trip on the transparent prefetch path;
219 /// a value <= 1 disables prefetch (restoring one @c SQLFetch per row).
220 LIGHTWEIGHT_API void SetDefaultPrefetchDepth(std::size_t depth) noexcept;
221
222 /// Creates a new query builder for the given table, compatible with the current connection.
223 ///
224 /// @param table The table to query.
225 /// If not provided, the query will be a generic query builder.
226 [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const& table = {}) const;
227
228 /// Creates a new query builder for the given table with an alias, compatible with the current connection.
229 ///
230 /// @param table The table to query.
231 /// @param tableAlias The alias to use for the table.
232 [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const& table,
233 std::string_view const& tableAlias) const;
234
235 /// Creates a new migration query builder, compatible the current connection.
236 [[nodiscard]] LIGHTWEIGHT_API SqlMigrationQueryBuilder Migration() const;
237
238 /// Tests if a transaction is active.
239 [[nodiscard]] LIGHTWEIGHT_API bool TransactionActive() const noexcept;
240
241 /// Tests if transactions are allowed.
242 [[nodiscard]] LIGHTWEIGHT_API bool TransactionsAllowed() const noexcept;
243
244 /// Tests if the connection is still active.
245 [[nodiscard]] LIGHTWEIGHT_API bool IsAlive() const noexcept;
246
247 /// Retrieves the connection information.
248 [[nodiscard]] LIGHTWEIGHT_API SqlConnectionString const& ConnectionString() const noexcept;
249
250 /// Retrieves the native handle.
251 [[nodiscard]] SQLHDBC NativeHandle() const noexcept
252 {
253 return m_hDbc;
254 }
255
256 /// Retrieves the last time the connection was used.
257 [[nodiscard]] LIGHTWEIGHT_API std::chrono::steady_clock::time_point LastUsed() const noexcept;
258
259 /// Sets the last time the connection was used.
260 LIGHTWEIGHT_API void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept;
261
262 /// Checks the result of an SQL operation, and throws an exception if it is not successful.
263 LIGHTWEIGHT_API void RequireSuccess(SQLRETURN sqlResult,
264 std::source_location sourceLocation = std::source_location::current()) const;
265
266 /// Enables coroutine-based asynchronous methods on this connection.
267 ///
268 /// Wires the connection to an injected execution context: blocking ODBC work is offloaded to
269 /// @p dbWorkers (serialized per connection so the ODBC handle is only ever used by one thread
270 /// at a time) and the awaiting coroutine is resumed via @p resume (typically the application's
271 /// run loop). A native driver-async backend is selected when the driver advertises support;
272 /// otherwise the portable thread-offload backend is used.
273 ///
274 /// Both executors must outlive this connection and every coroutine driven through it.
275 /// Must not be called while asynchronous operations are in flight on this connection (it
276 /// replaces the backend); the connection pool calls @ref DisableAsync on return so each
277 /// re-acquire is a fresh enable rather than a live replacement.
278 ///
279 /// @param dbWorkers The worker-thread pool used to run blocking ODBC calls.
280 /// @param resume The scheduler used to resume coroutines after a blocking step completes.
281 LIGHTWEIGHT_API void EnableAsync(Async::IExecutor& dbWorkers, Async::IResumeScheduler& resume);
282
283 /// Enables the asynchronous API on this connection using an explicitly provided backend.
284 ///
285 /// This is the dependency-injection seam behind the convenience overload above: callers and
286 /// tests can supply any @ref Async::IAsyncBackend implementation (a fake/inline backend for
287 /// tests, or a future native event backend) instead of the default thread-offload backend.
288 /// The same in-flight / lifetime constraints as the convenience overload apply.
289 ///
290 /// @param backend The async execution backend to install (consumed); must not be null.
291 LIGHTWEIGHT_API void EnableAsync(std::unique_ptr<Async::IAsyncBackend> backend);
292
293 /// Tears down the asynchronous backend, returning the connection to a non-async state.
294 ///
295 /// Called by the connection pool when a mapper is returned, so a recycled connection never
296 /// carries a stale backend that references executors which may have been destroyed. Safe to
297 /// call when async was never enabled.
298 ///
299 /// @warning Must not be called while async work is in flight on this connection: it destroys the
300 /// backend (and the strand/executors an outstanding offloaded step still references), which would
301 /// race the worker still touching the ODBC handle. Await every async operation on this connection
302 /// to completion before disabling (or before returning the owning pooled @c DataMapper).
303 LIGHTWEIGHT_API void DisableAsync() noexcept;
304
305 /// @return true if @ref EnableAsync has been called on this connection (and not yet disabled).
306 [[nodiscard]] LIGHTWEIGHT_API bool IsAsyncEnabled() const noexcept;
307
308 /// Retrieves the connection's asynchronous execution backend.
309 ///
310 /// Non-const because using the backend schedules and serializes real ODBC work (via its strand and
311 /// resume scheduler), which is an observable mutation of the connection's execution state.
312 ///
313 /// @pre @ref IsAsyncEnabled returns true.
314 /// @throws std::logic_error if async has not been enabled (programmer error, fail-fast).
315 /// @return The async backend used by this connection's async methods.
316 [[nodiscard]] LIGHTWEIGHT_API Async::IAsyncBackend& AsyncBackend();
317
318 private:
319 /// Ensures ODBC handles are allocated. Called by Connect() methods.
320 /// If handles were freed by Close(), this method reallocates them.
321 void EnsureHandlesAllocated();
322
323 void PostConnect();
324
325 // Private data members
326 // Note: move/move assignment operators implemented manually
327 // if adding new data members, make sure to update them accordingly.
328 SQLHENV m_hEnv {};
329 SQLHDBC m_hDbc {};
330 uint64_t m_connectionId;
331 SqlServerType m_serverType = SqlServerType::UNKNOWN;
332 SqlQueryFormatter const* m_queryFormatter {};
333 std::string m_driverName;
334
335 struct Data;
336 Data* m_data {};
337};
338
339inline SqlServerType SqlConnection::ServerType() const noexcept
340{
341 return m_serverType;
342}
343
344inline std::string const& SqlConnection::DriverName() const noexcept
345{
346 return m_driverName;
347}
348
350{
351 return *m_queryFormatter;
352}
353
354inline bool SqlConnection::SupportsNativeRowBatch() const noexcept
355{
356 // Native ODBC parameter-array binding (SQL_ATTR_PARAMSET_SIZE > 1) is a per-driver capability.
357 // Every backend Lightweight supports and tests against honours it; an unverified backend takes the
358 // always-correct per-row path rather than risk a driver that silently ignores the parameter array.
359 switch (ServerType())
360 {
361 case SqlServerType::MICROSOFT_SQL:
362 case SqlServerType::POSTGRESQL:
363 case SqlServerType::SQLITE:
364 return true;
365 case SqlServerType::MYSQL:
366 case SqlServerType::UNKNOWN:
367 return false;
368 }
369 return false;
370}
371
372inline bool SqlConnection::SupportsNativeRowArrayFetch(SqlServerType serverType) noexcept
373{
374 // Native ODBC row-array fetching (SQL_ATTR_ROW_ARRAY_SIZE > 1 + SQLFetchScroll) is a per-driver
375 // capability. Every backend Lightweight supports and tests against honours it; an unverified backend
376 // takes the always-correct per-row SQLFetch path rather than risk a driver that mis-handles the array.
377 switch (serverType)
378 {
379 case SqlServerType::MICROSOFT_SQL:
380 case SqlServerType::POSTGRESQL:
381 case SqlServerType::SQLITE:
382 return true;
383 case SqlServerType::MYSQL:
384 case SqlServerType::UNKNOWN:
385 return false;
386 }
387 return false;
388}
389
391{
393}
394
395inline bool SqlConnection::RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept
396{
397 switch (serverType)
398 {
399 case SqlServerType::MICROSOFT_SQL:
400 case SqlServerType::SQLITE:
401 return true;
402 case SqlServerType::POSTGRESQL: // psqlODBC transcodes SQL_C_CHAR through the client codepage
403 case SqlServerType::MYSQL:
404 case SqlServerType::UNKNOWN:
405 return false;
406 }
407 return false;
408}
409
410} // namespace Lightweight
Represents a connection to a SQL database.
LIGHTWEIGHT_API void RequireSuccess(SQLRETURN sqlResult, std::source_location sourceLocation=std::source_location::current()) const
Checks the result of an SQL operation, and throws an exception if it is not successful.
static LIGHTWEIGHT_API void ResetPostConnectedHook()
Resets the post connected hook.
LIGHTWEIGHT_API void DisableAsync() noexcept
LIGHTWEIGHT_API std::string ServerName() const
Retrieves the name of the server.
SqlServerType ServerType() const noexcept
Retrieves the type of the server.
LIGHTWEIGHT_API SqlMigrationQueryBuilder Migration() const
Creates a new migration query builder, compatible the current connection.
LIGHTWEIGHT_API bool TransactionActive() const noexcept
Tests if a transaction is active.
LIGHTWEIGHT_API bool IsAlive() const noexcept
Tests if the connection is still active.
LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const &table={}) const
LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const &table, std::string_view const &tableAlias) const
LIGHTWEIGHT_API SqlConnection(std::optional< SqlConnectionString > connectInfo)
Constructs a new SQL connection to the given connect informaton.
LIGHTWEIGHT_API bool TransactionsAllowed() const noexcept
Tests if transactions are allowed.
uint64_t ConnectionId() const noexcept
Retrieves the connection ID.
static bool RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept
Whether serverType's driver round-trips narrow (SQL_C_CHAR) character data byte-exact,...
SqlQueryFormatter const & QueryFormatter() const noexcept
Retrieves a query formatter suitable for the SQL server being connected.
bool SupportsNativeRowArrayFetch() const noexcept
Whether this connection's ODBC driver supports native row-array fetching (SQL_ATTR_ROW_ARRAY_SIZE > 1...
LIGHTWEIGHT_API void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept
Sets the last time the connection was used.
LIGHTWEIGHT_API void Close() noexcept
LIGHTWEIGHT_API bool IsAsyncEnabled() const noexcept
LIGHTWEIGHT_API Async::IAsyncBackend & AsyncBackend()
static LIGHTWEIGHT_API void SetDefaultDataSource(SqlConnectionDataSource const &dataSource) noexcept
Sets the default connection information as SqlConnectionDataSource.
static LIGHTWEIGHT_API SqlConnectionString const & DefaultConnectionString() noexcept
Retrieves the default connection information.
LIGHTWEIGHT_API void SetDefaultPrefetchDepth(std::size_t depth) noexcept
Sets the default block-prefetch depth for statements created on this connection.
LIGHTWEIGHT_API SqlErrorInfo LastError() const
Retrieves the last error information with respect to this SQL connection handle.
LIGHTWEIGHT_API SqlConnection()
Constructs a new SQL connection to the default connection.
LIGHTWEIGHT_API SqlConnection(SqlConnection &&) noexcept
Move constructor.
static LIGHTWEIGHT_API void SetDefaultConnectionString(SqlConnectionString const &connectionString) noexcept
LIGHTWEIGHT_API std::string DatabaseName() const
Retrieves the name of the database in use.
bool SupportsNativeRowBatch() const noexcept
Whether this connection's ODBC driver supports native parameter-array binding (SQL_ATTR_PARAMSET_SIZE...
LIGHTWEIGHT_API std::size_t DefaultPrefetchDepth() const noexcept
The default block-prefetch depth applied to statements created on this connection.
LIGHTWEIGHT_API std::chrono::steady_clock::time_point LastUsed() const noexcept
Retrieves the last time the connection was used.
LIGHTWEIGHT_API bool RequiresTableRebuildForSchemaChange() const noexcept
Whether this backend must rebuild a table to apply an ALTER TABLE schema change it cannot express in ...
SQLHDBC NativeHandle() const noexcept
Retrieves the native handle.
LIGHTWEIGHT_API std::string ServerVersion() const
Retrieves the reported server version.
LIGHTWEIGHT_API SqlConnectionString const & ConnectionString() const noexcept
Retrieves the connection information.
std::string const & DriverName() const noexcept
Retrieves the name of the driver used for this connection.
LIGHTWEIGHT_API std::string UserName() const
Retrieves the name of the user.
static LIGHTWEIGHT_API void SetPostConnectedHook(std::function< void(SqlConnection &)> hook)
Sets a callback to be called after each connection being established.
LIGHTWEIGHT_API void EnableAsync(Async::IExecutor &dbWorkers, Async::IResumeScheduler &resume)
LIGHTWEIGHT_API bool Connect(SqlConnectionDataSource const &info) noexcept
Query builder for building SQL migration queries.
Definition Migrate.hpp:477
API Entry point for building SQL queries.
Definition SqlQuery.hpp:32
API to format SQL queries for different SQL dialects.
Represents an ODBC SQL error.
Definition SqlError.hpp:32