Lightweight 0.20261023.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 "SqlRetryPolicy.hpp"
13#include "SqlServerType.hpp"
14
15#include <atomic>
16#include <chrono>
17#include <cstdint>
18#include <expected>
19#include <format>
20#include <functional>
21#include <memory>
22#include <optional>
23#include <string>
24#include <string_view>
25#include <system_error>
26
27#include <sql.h>
28#include <sqlext.h>
29#include <sqlspi.h>
30#include <sqltypes.h>
31
32/// @defgroup CoreApi Core API
33/// @brief The low-level SQL API: connections, statements, result cursors and transactions.
34///
35/// This is the thin layer directly over ODBC. Use it when you want to write SQL yourself and
36/// keep full control over binding and fetching. The higher-level @ref DataMapper and
37/// @ref QueryBuilder layers are built on top of these types and interoperate with them.
38
39namespace Lightweight
40{
41
42class SqlQueryBuilder;
43class SqlMigrationQueryBuilder;
44class SqlQueryFormatter;
45class SqlPreparedStatementCache;
46
47/// @ingroup CoreApi
48/// @brief How a connection treats a string or binary value written into a column too small to hold it.
49///
50/// The mode is applied per connection. On Microsoft SQL Server it is realised by toggling the
51/// session's @c ANSI_WARNINGS setting when the connection is established.
52enum class SqlStringTruncationMode : uint8_t
53{
54 /// The value is silently shortened to the column width (@c SET @c ANSI_WARNINGS @c OFF on SQL
55 /// Server). This matches the historical behaviour of some legacy data layers that clamp a value to
56 /// the column width before sending it.
57 ///
58 /// @warning On SQL Server this is @c ANSI_WARNINGS @c OFF, which also turns an arithmetic overflow
59 /// into a NULL rather than an error and suppresses the NULL-in-aggregate warning, and
60 /// makes INSERT/UPDATE fail on tables carrying an indexed view, a filtered index, or an
61 /// index over a computed column. Prefer @ref Error unless bug-for-bug compatibility with
62 /// such a legacy layer is required.
64
65 /// The server rejects the write with an error (@c SET @c ANSI_WARNINGS @c ON on SQL Server, which
66 /// is also the server default). No data is silently lost.
67 Error,
68};
69
70/// @ingroup CoreApi
71/// @brief The SQLite journal mode a connection requests when it is established.
72///
73/// Each enumerator other than @ref Unchanged maps onto the matching @c PRAGMA @c journal_mode value.
74/// See https://sqlite.org/pragma.html#pragma_journal_mode for the semantics of each mode.
75///
76/// @note SQLite may decline a requested mode without reporting an error, e.g. an in-memory database
77/// can only use @c MEMORY or @c OFF, so asking it for @ref Wal leaves it in @c MEMORY.
78enum class SqliteJournalMode : uint8_t
79{
80 /// Leave the journal mode alone: whatever the database file (or the driver) already uses stays in
81 /// effect. This is the default, so a connection that does not opt in behaves exactly as before.
83
84 /// @c PRAGMA @c journal_mode=DELETE — the rollback journal, deleted at the end of each transaction
85 /// (SQLite's own default).
86 Delete,
87
88 /// @c PRAGMA @c journal_mode=TRUNCATE — the rollback journal, truncated rather than deleted.
90
91 /// @c PRAGMA @c journal_mode=PERSIST — the rollback journal, kept and its header zeroed.
92 Persist,
93
94 /// @c PRAGMA @c journal_mode=MEMORY — the rollback journal, kept in memory.
95 Memory,
96
97 /// @c PRAGMA @c journal_mode=WAL — write-ahead logging, in which readers and writers do not block
98 /// each other. The mode is persistent: it is recorded in the database file and stays in effect for
99 /// every later connection to it. Requires a file database.
100 Wal,
101
102 /// @c PRAGMA @c journal_mode=OFF — no rollback journal; a crash or ROLLBACK can corrupt the
103 /// database.
104 Off,
105};
106
107/// @brief Default SQLite busy timeout: how long a statement waits on a database file locked by another
108/// connection before failing with "database is locked".
109inline constexpr std::chrono::milliseconds SqliteBusyTimeoutDefault { 60'000 };
110
111/// @ingroup CoreApi
112/// @brief Connection-level settings applied when a connection to a SQLite database is established.
113///
114/// Inert on every other backend: no statement is issued for them there. The defaults reproduce the
115/// behaviour Lightweight has always had, a 60 second busy timeout and an untouched journal mode.
116///
117/// @see SqlConnection::SetDefaultSqliteSettings, SqlConnection::SetSqliteSettings
119{
120 /// @brief How long a statement waits on a locked database file before failing, applied via
121 /// @c PRAGMA @c busy_timeout.
122 ///
123 /// Zero disables the wait, so a contended statement fails at once; a negative value is treated as
124 /// zero. Not to be confused with the ODBC login timeout of the connection string.
125 std::chrono::milliseconds busyTimeout = SqliteBusyTimeoutDefault;
126
127 /// The journal mode to switch the database to, see @ref SqliteJournalMode.
129
130 /// Equality comparison operator.
131 bool operator==(SqliteConnectionSettings const&) const noexcept = default;
132};
133
134/// @ingroup CoreApi
135/// @brief How a connection relates to its ODBC handles.
136enum class SqlHandleOwnership : uint8_t
137{
138 /// The connection allocated its handles, connects them and frees them on Close().
139 Owned,
140
141 /// Another component allocated and connected the handle and keeps owning it; the connection only
142 /// runs statements on it (see SqlConnection::Borrow).
143 Borrowed,
144};
145
146/// @ingroup CoreApi
147/// @brief Represents a connection to a SQL database.
148class SqlConnection final
149{
150 public:
151 /// @brief Constructs a new SQL connection to the default connection.
152 ///
153 /// The default connection is set via SetDefaultConnectInfo.
154 /// In case the default connection is not set, the connection will fail.
155 /// And in case the connection fails, the last error will be set.
156 LIGHTWEIGHT_API SqlConnection();
157
158 /// @brief Constructs a new SQL connection to the given connect informaton.
159 ///
160 /// @param connectInfo The connection information to use. If `std::nullopt`,
161 /// no connection is established and the object is left
162 /// in an unconnected state — use `Connect(...)` later.
163 /// If a value is provided and the connection fails,
164 /// `SqlException` is thrown carrying the ODBC diagnostic
165 /// read from the DBC handle.
166 LIGHTWEIGHT_API explicit SqlConnection(std::optional<SqlConnectionString> connectInfo);
167
168 /// @brief Constructs a new SQL connection with explicit SQLite connection settings.
169 ///
170 /// Behaves like the single-argument constructor, except that the connection uses
171 /// @p sqliteSettings instead of a snapshot of @ref DefaultSqliteSettings. The settings are applied
172 /// as part of establishing the connection, so the first statement already runs under them.
173 ///
174 /// @param connectInfo The connection information to use, or `std::nullopt` to stay unconnected.
175 /// @param sqliteSettings The SQLite settings applied on this and every later Connect(); inert on
176 /// other backends.
177 LIGHTWEIGHT_API SqlConnection(std::optional<SqlConnectionString> connectInfo, SqliteConnectionSettings sqliteSettings);
178
179 /// Move constructor.
180 LIGHTWEIGHT_API SqlConnection(SqlConnection&& /*other*/) noexcept;
181 /// Move assignment operator.
182 LIGHTWEIGHT_API SqlConnection& operator=(SqlConnection&& /*other*/) noexcept;
183 SqlConnection(SqlConnection const& /*other*/) = delete;
184 SqlConnection& operator=(SqlConnection const& /*other*/) = delete;
185
186 /// Destructs this SQL connection object,
187 LIGHTWEIGHT_API ~SqlConnection() noexcept;
188
189 /// @brief Wraps an ODBC connection handle that another component allocated, connected and keeps owning.
190 ///
191 /// Statements run on the borrowed connection share the owner's session, including any transaction
192 /// it has open. The server type, query formatter and driver name are detected as Connect() does,
193 /// but no session setting is applied: neither the string-truncation mode, nor the SQLite settings,
194 /// nor the post-connected hook. StringTruncationMode() reports the mode the session already has
195 /// (read from @c ANSI_WARNINGS on Microsoft SQL Server). Close() and the destructor detach from the
196 /// handle without disconnecting or freeing it, and Connect() on a borrowed connection fails.
197 ///
198 /// @param connectedHandle A connected ODBC connection handle. It must stay valid until the returned
199 /// connection is closed or destroyed, since both free the statement handles
200 /// cached on it.
201 /// @return A connection running on @p connectedHandle.
202 /// @throws std::invalid_argument if @p connectedHandle is null.
203 [[nodiscard]] LIGHTWEIGHT_API static SqlConnection Borrow(SQLHDBC connectedHandle);
204
205 /// @brief Tells whether this connection owns its ODBC handles or borrowed them (see @ref Borrow).
206 /// @return @ref SqlHandleOwnership::Borrowed for a connection made by @ref Borrow, else
207 /// @ref SqlHandleOwnership::Owned.
208 [[nodiscard]] LIGHTWEIGHT_API SqlHandleOwnership HandleOwnership() const noexcept;
209
210 /// Retrieves the default connection information.
211 ///
212 /// Thread-safe: the reference is to the calling thread's own snapshot of the default, refreshed
213 /// when the application has replaced it since (see @ref SetDefaultConnectionString), so another
214 /// thread replacing the default never changes the string under a reader. The same thread's next
215 /// call may, so copy it if it must stay fixed across such a switch.
216 ///
217 /// @return The calling thread's snapshot of the current default connection string.
218 [[nodiscard]] LIGHTWEIGHT_API static SqlConnectionString const& DefaultConnectionString();
219
220 /// Identifies the current default connection string.
221 ///
222 /// Incremented by every @ref SetDefaultConnectionString and @ref SetDefaultDataSource, so anything
223 /// that caches a connection made from the default - a connection pool, the migration manager's
224 /// mapper - can tell that it now points at a database the application has switched away from.
225 /// Compare for equality only; the counter wraps.
226 ///
227 /// Read it *before* reading @ref DefaultConnectionString(), since the setters publish the new string
228 /// first and the new generation second, so a connection stamped that way can only ever be
229 /// considered older than it is, never newer.
230 ///
231 /// @return The generation of the current default connection string.
232 [[nodiscard]] LIGHTWEIGHT_API static std::uint32_t DefaultConnectionStringGeneration() noexcept;
233
234 /// Sets the default connection information.
235 ///
236 /// Thread-safe. Connections already made from the previous default are not touched; pooled ones
237 /// are retired as they are next borrowed or returned (see @ref DefaultConnectionStringGeneration).
238 ///
239 /// @param connectionString The connection information to use.
240 LIGHTWEIGHT_API static void SetDefaultConnectionString(SqlConnectionString const& connectionString);
241
242 /// Sets the default connection information as SqlConnectionDataSource.
243 ///
244 /// @param dataSource The data source to flatten into the default connection string.
245 /// @see SetDefaultConnectionString
246 LIGHTWEIGHT_API static void SetDefaultDataSource(SqlConnectionDataSource const& dataSource);
247
248 /// Sets a callback to be called after each connection being established.
249 LIGHTWEIGHT_API static void SetPostConnectedHook(std::function<void(SqlConnection&)> hook);
250
251 /// Resets the post connected hook.
252 LIGHTWEIGHT_API static void ResetPostConnectedHook();
253
254 /// The string-truncation mode a newly established connection adopts. Defaults to
255 /// @ref SqlStringTruncationMode::Truncate.
256 [[nodiscard]] LIGHTWEIGHT_API static SqlStringTruncationMode DefaultStringTruncationMode() noexcept;
257
258 /// Sets the string-truncation mode newly established connections adopt. Existing connections are
259 /// unaffected; use @ref SetStringTruncationMode on those.
260 LIGHTWEIGHT_API static void SetDefaultStringTruncationMode(SqlStringTruncationMode mode) noexcept;
261
262 /// This connection's string-truncation mode. See @ref SqlStringTruncationMode.
263 [[nodiscard]] SqlStringTruncationMode StringTruncationMode() const noexcept
264 {
265 return m_stringTruncationMode;
266 }
267
268 /// Sets this connection's string-truncation mode, applying it immediately when the connection is
269 /// open (a no-op on backends other than Microsoft SQL Server).
271
272 /// @brief The SQLite settings a newly constructed connection adopts.
273 ///
274 /// Thread-safe. Defaults to a default-constructed @ref SqliteConnectionSettings (60 second busy
275 /// timeout, journal mode unchanged).
276 ///
277 /// @return A copy of the current process-wide default.
278 [[nodiscard]] LIGHTWEIGHT_API static SqliteConnectionSettings DefaultSqliteSettings();
279
280 /// @brief Sets the SQLite settings newly constructed connections adopt.
281 ///
282 /// Thread-safe. This is the way to configure connections that Lightweight constructs on the
283 /// application's behalf, such as those of a @c Pool or a default-constructed @c DataMapper.
284 /// Connections that already exist keep their settings; use @ref SetSqliteSettings on those.
285 ///
286 /// @param settings The new process-wide default.
287 LIGHTWEIGHT_API static void SetDefaultSqliteSettings(SqliteConnectionSettings const& settings);
288
289 /// @brief This connection's SQLite settings.
290 ///
291 /// Captured from @ref DefaultSqliteSettings when the connection object is constructed (or given to
292 /// the constructor), and re-applied on every Connect().
293 ///
294 /// @return The settings this connection applies when connected to SQLite.
295 [[nodiscard]] LIGHTWEIGHT_API SqliteConnectionSettings const& SqliteSettings() const noexcept;
296
297 /// @brief Changes this connection's SQLite settings, applying them immediately when the connection
298 /// is open to a SQLite database.
299 ///
300 /// On other backends, or while unconnected, the settings are only stored, to be applied by the next
301 /// Connect() to a SQLite database.
302 ///
303 /// @param settings The settings to store and apply.
304 /// @return Nothing on success, or the diagnostic of the @c PRAGMA statement that failed. The
305 /// settings are stored either way.
306 [[nodiscard]] LIGHTWEIGHT_API std::expected<void, SqlErrorInfo> SetSqliteSettings(
307 SqliteConnectionSettings const& settings);
308
309 /// @brief The connect-retry settings a newly constructed connection adopts.
310 ///
311 /// Thread-safe. Defaults to no retries (@c maxRetries of zero), so a failed connect fails at once
312 /// unless the application opts in.
313 ///
314 /// @return A copy of the current process-wide default.
315 [[nodiscard]] LIGHTWEIGHT_API static SqlRetrySettings DefaultConnectRetrySettings();
316
317 /// @brief Sets the connect-retry settings newly constructed connections adopt.
318 ///
319 /// Thread-safe. This is the way to make the connections that Lightweight opens on the application's
320 /// behalf (a @c Pool, a default-constructed @c DataMapper, migrations, backups) ride out a database
321 /// that is briefly unreachable. Connections that already exist keep their settings; use
322 /// @ref SetConnectRetrySettings on those.
323 ///
324 /// @param settings The new process-wide default.
325 LIGHTWEIGHT_API static void SetDefaultConnectRetrySettings(SqlRetrySettings const& settings);
326
327 /// @brief This connection's connect-retry settings.
328 ///
329 /// Captured from @ref DefaultConnectRetrySettings when the connection object is constructed, and
330 /// applied by every Connect().
331 ///
332 /// @return The backoff configuration Connect() retries with.
333 [[nodiscard]] LIGHTWEIGHT_API SqlRetrySettings const& ConnectRetrySettings() const noexcept;
334
335 /// @brief Changes how this connection's Connect() retries a connect that failed transiently.
336 ///
337 /// Connect() retries only failures that the dialect-agnostic classifier (@ref GenericRetryOps)
338 /// deems transient — a connection that could not be established or was dropped (SQLSTATE class
339 /// @c 08), a timeout (@c HYT00 / @c HYT01) — and fails at once on anything else, such as rejected
340 /// credentials. Each retry is reported through @ref SqlLogger::OnWarning.
341 ///
342 /// @param settings The backoff configuration; a @c maxRetries of zero disables retrying.
343 /// @param sleeper How to wait between attempts; @c nullptr selects @ref ThreadSleeper(). It must
344 /// outlive the connection. Injected so tests need not wait out real delays.
345 LIGHTWEIGHT_API void SetConnectRetrySettings(SqlRetrySettings const& settings,
346 SqlRetrySleeper* sleeper = nullptr) noexcept;
347
348 /// @brief Retrieves the connection ID.
349 ///
350 /// This is a unique identifier for the connection, which is useful for debugging purposes.
351 /// Note, this ID will not change if the connection is moved nor when it is reused via the connection pool.
352 [[nodiscard]] uint64_t ConnectionId() const noexcept
353 {
354 return m_connectionId;
355 }
356
357 /// Closes the connection, freeing the underlying ODBC handles.
358 ///
359 /// A borrowed connection (see @ref Borrow) only detaches from its handle, leaving it connected.
360 ///
361 /// This does not return the connection to any pool: `SqlConnection` owns its handles outright.
362 /// Connection pooling is provided separately by `Lightweight::Pool` in
363 /// `DataMapper/Pool.hpp`, which recycles pooled `DataMapper` instances.
364 LIGHTWEIGHT_API void Close() noexcept;
365
366 /// Connects to the given database with the given username and password.
367 ///
368 /// This method can be called on a connection that has been closed via Close().
369 /// If the ODBC handles have been freed, they will be automatically reallocated.
370 /// A transient failure is retried as @ref SetConnectRetrySettings configures.
371 /// It always fails on a borrowed connection (see @ref Borrow).
372 ///
373 /// @retval true if the connection was successful.
374 /// @retval false if the connection failed. Use LastError() to retrieve the error information.
375 LIGHTWEIGHT_API bool Connect(SqlConnectionDataSource const& info) noexcept;
376
377 /// Connects to the given database with the given connection string.
378 ///
379 /// This method can be called on a connection that has been closed via Close().
380 /// If the ODBC handles have been freed, they will be automatically reallocated.
381 /// A transient failure is retried as @ref SetConnectRetrySettings configures.
382 /// It always fails on a borrowed connection (see @ref Borrow).
383 ///
384 /// @retval true if the connection was successful.
385 /// @retval false if the connection failed. Use LastError() to retrieve the error information.
386 LIGHTWEIGHT_API bool Connect(SqlConnectionString sqlConnectionString) noexcept;
387
388 /// Retrieves the last error information with respect to this SQL connection handle.
389 [[nodiscard]] LIGHTWEIGHT_API SqlErrorInfo LastError() const;
390
391 /// Retrieves the name of the database in use.
392 [[nodiscard]] LIGHTWEIGHT_API std::string DatabaseName() const;
393
394 /// Retrieves the name of the user.
395 [[nodiscard]] LIGHTWEIGHT_API std::string UserName() const;
396
397 /// Retrieves the name of the server.
398 [[nodiscard]] LIGHTWEIGHT_API std::string ServerName() const;
399
400 /// Retrieves the reported server version.
401 [[nodiscard]] LIGHTWEIGHT_API std::string ServerVersion() const;
402
403 /// Retrieves the type of the server.
404 [[nodiscard]] SqlServerType ServerType() const noexcept;
405
406 /// Retrieves the name of the driver used for this connection.
407 [[nodiscard]] std::string const& DriverName() const noexcept;
408
409 /// Tells whether text reaches the driver intact through the ODBC wide (W) entry points.
410 ///
411 /// Off Windows, a driver that implements only the ANSI entry points (such as the SQLite ODBC
412 /// driver some Linux distributions ship) receives wide text from the driver manager narrowed one
413 /// UTF-16 code unit to one byte, which corrupts every non-ASCII character. For such a driver the
414 /// library hands over query text, catalog-function arguments and connection text as UTF-8
415 /// through the ANSI entry points, and reads its diagnostics through them. Always true on Windows,
416 /// whose driver manager converts wide text to the code page an ANSI driver expects.
417 [[nodiscard]] bool AcceptsWideQueryText() const noexcept;
418
419 /// Retrieves a query formatter suitable for the SQL server being connected.
420 [[nodiscard]] SqlQueryFormatter const& QueryFormatter() const noexcept;
421
422 /// @brief Whether this backend must rebuild a table to apply an `ALTER TABLE` schema change it
423 /// cannot express in place (foreign-key add/drop, or column type/nullability change).
424 ///
425 /// The migration executor consults this to decide whether a `-- LIGHTWEIGHT_SQLITE_GUARD:` sentinel
426 /// must be turned into a table rebuild (`true` for SQLite) or executed directly. Exposed on the
427 /// connection so capability decisions go through the backend, delegating the dialect knowledge to
428 /// the query formatter.
429 [[nodiscard]] LIGHTWEIGHT_API bool RequiresTableRebuildForSchemaChange() const noexcept;
430
431 /// @brief Whether this connection's ODBC driver supports native parameter-array binding
432 /// (`SQL_ATTR_PARAMSET_SIZE` > 1) for batched row-wise execution.
433 ///
434 /// The batched `SqlStatement::ExecuteBatch(rows, accessors...)` consults this to decide whether it
435 /// may submit the whole batch in a single row-wise `SQLExecute`, or must fall back to a single
436 /// prepare followed by consecutive per-row executes. This is a driver/backend capability — not a
437 /// SQL-dialect concern — so it belongs to the connection, which knows both the server type and the
438 /// driver name (either of which a future carve-out can branch on).
439 ///
440 /// @return `true` if the driver honours parameter arrays.
441 [[nodiscard]] bool SupportsNativeRowBatch() const noexcept;
442
443 /// @brief Whether this connection's ODBC driver supports native row-array fetching
444 /// (`SQL_ATTR_ROW_ARRAY_SIZE` > 1 with `SQLFetchScroll`) for block result retrieval.
445 ///
446 /// The fast retrieval path in @c SqlStatement::FetchAllRowWise consults this to decide whether
447 /// it may bind the result columns row-wise over a record block and materialize whole row blocks per
448 /// `SQLFetchScroll` round-trip, or must fall back to the per-row `SQLFetch` path. Like
449 /// @ref SupportsNativeRowBatch this is a driver/backend capability — not a SQL-dialect concern — so
450 /// it lives on the connection. Kept distinct from the parameter-array flag so a backend that honours
451 /// one but not the other can be carved out independently.
452 ///
453 /// @return `true` if the driver honours row-array fetching.
454 [[nodiscard]] bool SupportsNativeRowArrayFetch() const noexcept;
455
456 /// @brief Server-type overload of @ref SupportsNativeRowArrayFetch, for callers that hold only the
457 /// server type (e.g. the DataMapper result reader) and not the connection. Keeps the single source of
458 /// truth for this capability on the connection rather than scattering a `switch (serverType)` into
459 /// business logic.
460 /// @param serverType The backend server type to test.
461 /// @return `true` if that backend honours row-array fetching.
462 [[nodiscard]] static bool SupportsNativeRowArrayFetch(SqlServerType serverType) noexcept;
463
464 /// @brief Whether @p serverType's driver round-trips narrow (@c SQL_C_CHAR) character data
465 /// byte-exact, so a fixed-capacity char string may be array-bound narrow on the row-wise fetch path.
466 ///
467 /// PostgreSQL's psqlODBC transcodes @c SQL_C_CHAR through the client codepage (cp1252 on Windows),
468 /// mangling non-ASCII bytes — its single-row binder therefore reads narrow strings via @c SQL_C_WCHAR.
469 /// That wide round-trip needs an external per-cell buffer + conversion, which cannot be expressed as an
470 /// in-place row-wise array bind, so a record carrying a fixed-capacity string falls back to the per-row
471 /// path on PostgreSQL. MS SQL Server and SQLite read @c SQL_C_CHAR verbatim and stay on the fast path.
472 ///
473 /// @param serverType The backend server type to test.
474 /// @return `true` if narrow character data round-trips byte-exact on that backend.
475 [[nodiscard]] static bool RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept;
476
477 /// @brief The default block-prefetch depth applied to statements created on this connection.
478 ///
479 /// Classic per-row fetch loops (`while (cursor.FetchRow()) ...`, @c SqlRowIterator,
480 /// @c SqlVariantRowCursor) transparently request this many rows per @c SQLFetchScroll round-trip
481 /// instead of issuing one @c SQLFetch per row. A value <= 1 disables prefetch. Effective only when
482 /// @ref SupportsNativeRowArrayFetch is true; otherwise statements fall back to per-row fetching.
483 ///
484 /// @return The configured default prefetch depth (defaults to @c PrefetchDepthDefault).
485 [[nodiscard]] LIGHTWEIGHT_API std::size_t DefaultPrefetchDepth() const noexcept;
486
487 /// @brief Sets the default block-prefetch depth for statements created on this connection.
488 ///
489 /// @param depth Rows to request per @c SQLFetchScroll round-trip on the transparent prefetch path;
490 /// a value <= 1 disables prefetch (restoring one @c SQLFetch per row).
491 LIGHTWEIGHT_API void SetDefaultPrefetchDepth(std::size_t depth) noexcept;
492
493 /// @brief Whether this connection's backend supports reusing an already-prepared statement handle
494 /// for a later execution of the same SQL text.
495 ///
496 /// Gates the prepared-statement cache: a backend not known to keep a prepared statement
497 /// re-executable across an intervening cursor close never pools handles, no matter what capacity
498 /// was configured. Like @ref SupportsNativeRowBatch this is a driver/backend capability rather than
499 /// a SQL-dialect concern, so it lives on the connection instead of a `switch` in the caller.
500 ///
501 /// @return `true` if prepared handles may be pooled and re-executed on this backend.
502 [[nodiscard]] bool SupportsPreparedStatementReuse() const noexcept;
503
504 /// @brief Server-type overload of @ref SupportsPreparedStatementReuse, for callers that hold only
505 /// the server type rather than a live connection.
506 ///
507 /// @param serverType The backend to answer for.
508 /// @return `true` if prepared handles may be pooled and re-executed on @p serverType.
509 [[nodiscard]] static bool SupportsPreparedStatementReuse(SqlServerType serverType) noexcept;
510
511 /// @brief The capacity of this connection's prepared-statement cache.
512 ///
513 /// @return The number of already-prepared statement handles kept for reuse; `0` when the cache is
514 /// disabled (the default).
515 [[nodiscard]] LIGHTWEIGHT_API std::size_t PreparedStatementCacheCapacity() const noexcept;
516
517 /// @brief Enables (or resizes) this connection's prepared-statement cache.
518 ///
519 /// With a non-zero capacity, `SqlStatement::Prepare()` on this connection first looks for an idle
520 /// handle already prepared for the same SQL text and reuses it, skipping the driver's `SQLPrepare`
521 /// round-trip. Handles return to the cache when the statement is re-prepared or destroyed, and are
522 /// evicted least-recently-used first once the bound is exceeded. Every layer built on
523 /// `SqlStatement` — `DataMapper`, the query builders — benefits without call-site changes; a single
524 /// statement can opt out via `SqlStatement::SetPreparedStatementCaching`.
525 ///
526 /// A capacity request on a backend without @ref SupportsPreparedStatementReuse is kept but stays
527 /// inactive, so the same setup code is safe to run against any DBMS.
528 ///
529 /// @warning A pooled handle carries the query plan the driver derived when it was prepared. Call
530 /// @ref ClearPreparedStatementCache after DDL that a cached query touches; Lightweight's
531 /// own migration executor and `SqlStatement::MigrateDirect` already do so.
532 ///
533 /// @param capacity Maximum number of idle prepared handles to keep; `0` disables and clears the
534 /// cache. @c PreparedStatementCacheCapacitySuggested is a reasonable starting point.
535 LIGHTWEIGHT_API void SetPreparedStatementCacheCapacity(std::size_t capacity) noexcept;
536
537 /// Frees every pooled prepared statement handle, e.g. after DDL invalidated the cached query plans.
538 LIGHTWEIGHT_API void ClearPreparedStatementCache() noexcept;
539
540 /// @brief Retrieves this connection's prepared-statement cache.
541 ///
542 /// Non-const because acquiring and releasing pooled handles mutates the cache. Mostly of interest
543 /// for its @c SqlPreparedStatementCache::Stats counters.
544 [[nodiscard]] LIGHTWEIGHT_API SqlPreparedStatementCache& PreparedStatementCache() noexcept;
545
546 /// Creates a new query builder for the given table, compatible with the current connection.
547 ///
548 /// @param table The table to query.
549 /// If not provided, the query will be a generic query builder.
550 [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const& table = {}) const;
551
552 /// Creates a new query builder for the given table with an alias, compatible with the current connection.
553 ///
554 /// @param table The table to query.
555 /// @param tableAlias The alias to use for the table.
556 [[nodiscard]] LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const& table,
557 std::string_view const& tableAlias) const;
558
559 /// Creates a new migration query builder, compatible the current connection.
560 [[nodiscard]] LIGHTWEIGHT_API SqlMigrationQueryBuilder Migration() const;
561
562 /// Tests if a transaction is active.
563 [[nodiscard]] LIGHTWEIGHT_API bool TransactionActive() const noexcept;
564
565 /// Rolls back a transaction left open on this connection and restores autocommit mode.
566 ///
567 /// Recognizes a transaction the ODBC way, by autocommit being off - as @ref SqlTransaction and
568 /// any caller switching autocommit off leave it. A transaction opened by executing a raw
569 /// @c BEGIN statement while autocommit is on is not visible to ODBC and is not ended here.
570 ///
571 /// The connection pool calls this on every connection returned to it.
572 ///
573 /// @return @c true when the connection is in autocommit mode afterwards (including when no
574 /// transaction was open); @c false when the rollback or the mode switch failed.
575 [[nodiscard]] LIGHTWEIGHT_API bool RollbackOpenTransaction() noexcept;
576
577 /// Tests if transactions are allowed.
578 [[nodiscard]] LIGHTWEIGHT_API bool TransactionsAllowed() const noexcept;
579
580 /// Tests if the connection is still active.
581 [[nodiscard]] LIGHTWEIGHT_API bool IsAlive() const noexcept;
582
583 /// Retrieves the connection information.
584 [[nodiscard]] LIGHTWEIGHT_API SqlConnectionString const& ConnectionString() const noexcept;
585
586 /// Retrieves the native handle.
587 [[nodiscard]] SQLHDBC NativeHandle() const noexcept
588 {
589 return m_hDbc;
590 }
591
592 /// Retrieves the last time the connection was used.
593 [[nodiscard]] LIGHTWEIGHT_API std::chrono::steady_clock::time_point LastUsed() const noexcept;
594
595 /// Sets the last time the connection was used.
596 LIGHTWEIGHT_API void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept;
597
598 /// Checks the result of an SQL operation, and throws an exception if it is not successful.
599 LIGHTWEIGHT_API void RequireSuccess(SQLRETURN sqlResult,
600 std::source_location sourceLocation = std::source_location::current()) const;
601
602 /// Enables coroutine-based asynchronous methods on this connection.
603 ///
604 /// Wires the connection to an injected execution context: blocking ODBC work is offloaded to
605 /// @p dbWorkers (serialized per connection so the ODBC handle is only ever used by one thread
606 /// at a time) and the awaiting coroutine is resumed via @p resume (typically the application's
607 /// run loop). A native driver-async backend is selected when the driver advertises support;
608 /// otherwise the portable thread-offload backend is used.
609 ///
610 /// Both executors must outlive this connection and every coroutine driven through it.
611 /// Must not be called while asynchronous operations are in flight on this connection (it
612 /// replaces the backend); the connection pool calls @ref DisableAsync on return so each
613 /// re-acquire is a fresh enable rather than a live replacement.
614 ///
615 /// @param dbWorkers The worker-thread pool used to run blocking ODBC calls.
616 /// @param resume The scheduler used to resume coroutines after a blocking step completes.
617 LIGHTWEIGHT_API void EnableAsync(Async::IExecutor& dbWorkers, Async::IResumeScheduler& resume);
618
619 /// Enables the asynchronous API on this connection using an explicitly provided backend.
620 ///
621 /// This is the dependency-injection seam behind the convenience overload above: callers and
622 /// tests can supply any @ref Async::IAsyncBackend implementation (a fake/inline backend for
623 /// tests, or a future native event backend) instead of the default thread-offload backend.
624 /// The same in-flight / lifetime constraints as the convenience overload apply.
625 ///
626 /// @param backend The async execution backend to install (consumed); must not be null.
627 LIGHTWEIGHT_API void EnableAsync(std::unique_ptr<Async::IAsyncBackend> backend);
628
629 /// Tears down the asynchronous backend, returning the connection to a non-async state.
630 ///
631 /// Called by the connection pool when a mapper is returned, so a recycled connection never
632 /// carries a stale backend that references executors which may have been destroyed. Safe to
633 /// call when async was never enabled.
634 ///
635 /// @warning Must not be called while async work is in flight on this connection: it destroys the
636 /// backend (and the strand/executors an outstanding offloaded step still references), which would
637 /// race the worker still touching the ODBC handle. Await every async operation on this connection
638 /// to completion before disabling (or before returning the owning pooled @c DataMapper).
639 LIGHTWEIGHT_API void DisableAsync() noexcept;
640
641 /// @return true if @ref EnableAsync has been called on this connection (and not yet disabled).
642 [[nodiscard]] LIGHTWEIGHT_API bool IsAsyncEnabled() const noexcept;
643
644 /// Retrieves the connection's asynchronous execution backend.
645 ///
646 /// Non-const because using the backend schedules and serializes real ODBC work (via its strand and
647 /// resume scheduler), which is an observable mutation of the connection's execution state.
648 ///
649 /// @pre @ref IsAsyncEnabled returns true.
650 /// @throws std::logic_error if async has not been enabled (programmer error, fail-fast).
651 /// @return The async backend used by this connection's async methods.
652 [[nodiscard]] LIGHTWEIGHT_API Async::IAsyncBackend& AsyncBackend();
653
654 private:
655 /// Selects the private constructor that borrows a handle, keeping it out of overload resolution
656 /// for the public constructors.
657 struct BorrowTag
658 {
659 };
660
661 /// Constructs a connection on a borrowed, already connected handle. Used by Borrow().
662 SqlConnection(BorrowTag /*tag*/, SQLHDBC connectedHandle);
663
664 /// Detects the server type, query formatter, driver name and text entry point of the connected handle.
665 void DetectServer();
666
667 /// Ensures ODBC handles are allocated. Called by Connect() methods.
668 /// If handles were freed by Close(), this method reallocates them.
669 void EnsureHandlesAllocated();
670
671 /// Detects the server type and applies the connect-time session settings.
672 /// @return `false` if a requested session setting could not be applied (the error is logged).
673 [[nodiscard]] bool PostConnect();
674
675 /// One attempt of Connect(SqlConnectionDataSource const&).
676 [[nodiscard]] std::expected<void, SqlErrorInfo> ConnectOnce(SqlConnectionDataSource const& info) noexcept;
677
678 /// One attempt of Connect(SqlConnectionString).
679 [[nodiscard]] std::expected<void, SqlErrorInfo> ConnectOnce() noexcept;
680
681 /// Runs @p attempt, retrying a transient failure as the connect-retry settings say.
682 /// @return True once an attempt succeeded; false when the policy gave up.
683 template <typename Attempt>
684 [[nodiscard]] bool ConnectWithRetry(Attempt const& attempt) noexcept;
685
686 /// Issues the statements the query formatter derives from this connection's SQLite settings; none
687 /// unless connected to SQLite.
688 /// @return Nothing on success, or the diagnostic of the statement that failed.
689 [[nodiscard]] std::expected<void, SqlErrorInfo> ApplySqliteSettings();
690
691 /// Re-evaluates the backend capability gate on the requested prepared-statement cache capacity.
692 void ApplyPreparedStatementCacheCapacity() noexcept;
693
694 /// Issues the SET statement that realises m_stringTruncationMode. A no-op unless connected to
695 /// Microsoft SQL Server. Throws if the SET statement fails, like the other connect-time settings.
696 void ApplyStringTruncationMode();
697
698 /// Adopts the string-truncation mode the session already has, without changing it. Reads the
699 /// @c ANSI_WARNINGS state on Microsoft SQL Server; keeps the current mode on every other backend.
700 void ReadStringTruncationMode();
701
702 // Private data members
703 // Note: move/move assignment operators implemented manually
704 // if adding new data members, make sure to update them accordingly.
705 SQLHENV m_hEnv {};
706 SQLHDBC m_hDbc {};
707 uint64_t m_connectionId;
708 SqlServerType m_serverType = SqlServerType::UNKNOWN;
709 SqlQueryFormatter const* m_queryFormatter {};
710 std::string m_driverName;
711 bool m_acceptsWideQueryText = true;
714
715 struct Data;
716 Data* m_data {};
717};
718
719inline SqlServerType SqlConnection::ServerType() const noexcept
720{
721 return m_serverType;
722}
723
724inline std::string const& SqlConnection::DriverName() const noexcept
725{
726 return m_driverName;
727}
728
729inline bool SqlConnection::AcceptsWideQueryText() const noexcept
730{
731 return m_acceptsWideQueryText;
732}
733
735{
736 return *m_queryFormatter;
737}
738
739inline bool SqlConnection::SupportsNativeRowBatch() const noexcept
740{
741 // Native ODBC parameter-array binding (SQL_ATTR_PARAMSET_SIZE > 1) is a per-driver capability.
742 // Every backend Lightweight supports and tests against honours it; an unverified backend takes the
743 // always-correct per-row path rather than risk a driver that silently ignores the parameter array.
744 switch (ServerType())
745 {
746 case SqlServerType::MICROSOFT_SQL:
747 case SqlServerType::POSTGRESQL:
748 case SqlServerType::SQLITE:
749 return true;
750 case SqlServerType::MYSQL:
751 case SqlServerType::UNKNOWN:
752 return false;
753 }
754 return false;
755}
756
757inline bool SqlConnection::SupportsPreparedStatementReuse(SqlServerType serverType) noexcept
758{
759 // Re-executing a prepared statement after its cursor was closed is core ODBC, but pooling handles
760 // across call sites is only claimed for the backends Lightweight tests against. An unverified
761 // backend keeps the always-correct prepare-per-statement path.
762 switch (serverType)
763 {
764 case SqlServerType::MICROSOFT_SQL:
765 case SqlServerType::POSTGRESQL:
766 case SqlServerType::SQLITE:
767 return true;
768 case SqlServerType::MYSQL:
769 case SqlServerType::UNKNOWN:
770 return false;
771 }
772 return false;
773}
774
779
780inline bool SqlConnection::SupportsNativeRowArrayFetch(SqlServerType serverType) noexcept
781{
782 // Native ODBC row-array fetching (SQL_ATTR_ROW_ARRAY_SIZE > 1 + SQLFetchScroll) is a per-driver
783 // capability. Every backend Lightweight supports and tests against honours it; an unverified backend
784 // takes the always-correct per-row SQLFetch path rather than risk a driver that mis-handles the array.
785 switch (serverType)
786 {
787 case SqlServerType::MICROSOFT_SQL:
788 case SqlServerType::POSTGRESQL:
789 case SqlServerType::SQLITE:
790 return true;
791 case SqlServerType::MYSQL:
792 case SqlServerType::UNKNOWN:
793 return false;
794 }
795 return false;
796}
797
799{
801}
802
803inline bool SqlConnection::RoundTripsNarrowTextByteExact(SqlServerType serverType) noexcept
804{
805 switch (serverType)
806 {
807 case SqlServerType::MICROSOFT_SQL:
808 case SqlServerType::SQLITE:
809 return true;
810 case SqlServerType::POSTGRESQL: // psqlODBC transcodes SQL_C_CHAR through the client codepage
811 case SqlServerType::MYSQL:
812 case SqlServerType::UNKNOWN:
813 return false;
814 }
815 return false;
816}
817
818} // 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 std::expected< void, SqlErrorInfo > SetSqliteSettings(SqliteConnectionSettings const &settings)
Changes this connection's SQLite settings, applying them immediately when the connection is open to a...
LIGHTWEIGHT_API void DisableAsync() noexcept
static LIGHTWEIGHT_API void SetDefaultConnectRetrySettings(SqlRetrySettings const &settings)
Sets the connect-retry settings newly constructed connections adopt.
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 SqlPreparedStatementCache & PreparedStatementCache() noexcept
Retrieves this connection's prepared-statement cache.
static LIGHTWEIGHT_API void SetDefaultSqliteSettings(SqliteConnectionSettings const &settings)
Sets the SQLite settings newly constructed connections adopt.
LIGHTWEIGHT_API std::size_t PreparedStatementCacheCapacity() const noexcept
The capacity of this connection's prepared-statement cache.
LIGHTWEIGHT_API bool IsAlive() const noexcept
Tests if the connection is still active.
LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const &table={}) const
static LIGHTWEIGHT_API SqlConnection Borrow(SQLHDBC connectedHandle)
Wraps an ODBC connection handle that another component allocated, connected and keeps owning.
LIGHTWEIGHT_API SqlQueryBuilder QueryAs(std::string_view const &table, std::string_view const &tableAlias) const
LIGHTWEIGHT_API void SetConnectRetrySettings(SqlRetrySettings const &settings, SqlRetrySleeper *sleeper=nullptr) noexcept
Changes how this connection's Connect() retries a connect that failed transiently.
static LIGHTWEIGHT_API SqlStringTruncationMode DefaultStringTruncationMode() noexcept
LIGHTWEIGHT_API SqlRetrySettings const & ConnectRetrySettings() const noexcept
This connection's connect-retry settings.
LIGHTWEIGHT_API SqlConnection(std::optional< SqlConnectionString > connectInfo)
Constructs a new SQL connection to the given connect informaton.
static LIGHTWEIGHT_API std::uint32_t DefaultConnectionStringGeneration() noexcept
LIGHTWEIGHT_API void SetPreparedStatementCacheCapacity(std::size_t capacity) noexcept
Enables (or resizes) this connection's prepared-statement cache.
LIGHTWEIGHT_API bool TransactionsAllowed() const noexcept
Tests if transactions are allowed.
LIGHTWEIGHT_API void ClearPreparedStatementCache() noexcept
Frees every pooled prepared statement handle, e.g. after DDL invalidated the cached query plans.
uint64_t ConnectionId() const noexcept
Retrieves the connection ID.
SqlStringTruncationMode StringTruncationMode() const noexcept
This connection's string-truncation mode. See SqlStringTruncationMode.
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...
static LIGHTWEIGHT_API SqlConnectionString const & DefaultConnectionString()
LIGHTWEIGHT_API void SetLastUsed(std::chrono::steady_clock::time_point lastUsed) noexcept
Sets the last time the connection was used.
static LIGHTWEIGHT_API SqliteConnectionSettings DefaultSqliteSettings()
The SQLite settings a newly constructed connection adopts.
LIGHTWEIGHT_API void Close() noexcept
LIGHTWEIGHT_API bool IsAsyncEnabled() const noexcept
LIGHTWEIGHT_API Async::IAsyncBackend & AsyncBackend()
LIGHTWEIGHT_API SqlConnection(std::optional< SqlConnectionString > connectInfo, SqliteConnectionSettings sqliteSettings)
Constructs a new SQL connection with explicit SQLite connection settings.
static LIGHTWEIGHT_API void SetDefaultConnectionString(SqlConnectionString const &connectionString)
bool AcceptsWideQueryText() const noexcept
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 SetDefaultStringTruncationMode(SqlStringTruncationMode mode) noexcept
LIGHTWEIGHT_API bool RollbackOpenTransaction() 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 ...
LIGHTWEIGHT_API SqlHandleOwnership HandleOwnership() const noexcept
Tells whether this connection owns its ODBC handles or borrowed them (see Borrow).
LIGHTWEIGHT_API void SetStringTruncationMode(SqlStringTruncationMode mode)
SQLHDBC NativeHandle() const noexcept
Retrieves the native handle.
bool SupportsPreparedStatementReuse() const noexcept
Whether this connection's backend supports reusing an already-prepared statement handle for a later e...
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.
static LIGHTWEIGHT_API void SetDefaultDataSource(SqlConnectionDataSource const &dataSource)
LIGHTWEIGHT_API SqliteConnectionSettings const & SqliteSettings() const noexcept
This connection's SQLite settings.
LIGHTWEIGHT_API std::string UserName() const
Retrieves the name of the user.
static LIGHTWEIGHT_API SqlRetrySettings DefaultConnectRetrySettings()
The connect-retry settings a newly constructed connection adopts.
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:521
A bounded LRU pool of already-prepared ODBC statement handles, owned by a SqlConnection.
API Entry point for building SQL queries.
Definition SqlQuery.hpp:32
API to format SQL queries for different SQL dialects.
Supplies the wait between retries.
SqliteJournalMode
The SQLite journal mode a connection requests when it is established.
SqlStringTruncationMode
How a connection treats a string or binary value written into a column too small to hold it.
SqlHandleOwnership
How a connection relates to its ODBC handles.
@ Memory
PRAGMA journal_mode=MEMORY — the rollback journal, kept in memory.
@ Persist
PRAGMA journal_mode=PERSIST — the rollback journal, kept and its header zeroed.
@ Owned
The connection allocated its handles, connects them and frees them on Close().
Represents an ODBC SQL error.
Definition SqlError.hpp:32
Connection-level settings applied when a connection to a SQLite database is established.
std::chrono::milliseconds busyTimeout
How long a statement waits on a locked database file before failing, applied via PRAGMA busy_timeout.
bool operator==(SqliteConnectionSettings const &) const noexcept=default
Equality comparison operator.
SqliteJournalMode journalMode
The journal mode to switch the database to, see SqliteJournalMode.