Lightweight 0.20260921.0
Loading...
Searching...
No Matches
SqlPreparedStatementCache.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 "SqlOdbcPrelude.hpp"
8
9#include <cstddef>
10#include <cstdint>
11#include <list>
12#include <optional>
13#include <string>
14#include <string_view>
15#include <unordered_map>
16
17#include <sql.h>
18#include <sqltypes.h>
19
20namespace Lightweight
21{
22
23/// @ingroup CoreApi
24/// @brief Whether a single @c SqlStatement takes part in its connection's prepared-statement cache.
25///
26/// Statements opt in by default, which only has an effect once the owning connection was given a
27/// non-zero cache capacity (see @c SqlConnection::SetPreparedStatementCacheCapacity). Individual
28/// call sites that must not reuse a plan — for instance a statement that straddles a schema change —
29/// opt out via @c SqlStatement::SetPreparedStatementCaching.
30enum class SqlPreparedStatementCaching : uint8_t
31{
32 /// Reuse a pooled handle when one matches, and hand the handle back to the pool afterwards.
33 Enabled,
34
35 /// Never take a handle from, nor give one to, the connection's cache.
37};
38
39/// @ingroup CoreApi
40/// @brief A bounded LRU pool of already-prepared ODBC statement handles, owned by a @c SqlConnection.
41///
42/// Preparing a statement costs a server-side parse on MS SQL Server and PostgreSQL. Neither driver pays
43/// it inside @c SQLPrepare, which sends nothing: it rides along with the first execute of the freshly
44/// prepared handle, and a matching deallocate follows when the handle is freed. This cache keeps the
45/// @c SQLHSTMT handles of recently prepared queries alive, so a repeat of the same SQL text on the same
46/// connection re-executes a prepared handle instead. Measured behind a 50 ms link, that is worth about
47/// three round-trips per query on psqlODBC and one on the Microsoft driver.
48///
49/// A handle is *checked out* while a statement uses it: @ref Acquire removes it from the pool and
50/// @ref Release puts it back. Two statements preparing the same query at the same time therefore each
51/// get their own handle, and both are pooled afterwards (subject to the capacity bound). Eviction is
52/// least-recently-released first, which matters because several backends cap the number of live
53/// prepared statements per session.
54///
55/// @note Not thread-safe, mirroring @c SqlConnection: one connection is used by one thread at a time.
56/// @note A pooled handle holds a query plan derived from the schema as it was at preparation time. A
57/// connection that runs DDL must drop those plans via
58/// @c SqlConnection::ClearPreparedStatementCache — Lightweight's own migration paths do it for you.
60{
61 public:
62 /// @brief A pooled statement handle together with the parameter count the driver reported for it.
64 {
65 /// The native ODBC statement handle, prepared for the associated query text.
66 SQLHSTMT nativeHandle {};
67
68 /// The number of input parameters @c SQLNumParams reported for that query.
69 SQLSMALLINT parameterCount {};
70 };
71
72 /// @brief Cumulative counters, primarily for tests and diagnostics.
74 {
75 /// Prepare requests served from the pool, i.e. reusing a handle the server already prepared.
76 uint64_t hits {};
77
78 /// Prepare requests that had to issue @c SQLPrepare.
79 uint64_t misses {};
80
81 /// Pooled handles freed because the capacity bound was exceeded.
82 uint64_t evictions {};
83
84 /// Prepare requests a statement served from the handle it was already holding, i.e. a repeat of
85 /// the query text it last prepared. Like a hit these cost no @c SQLPrepare, but they never touch
86 /// the pool: parking the handle only to look that same text back up would be pure overhead.
87 uint64_t directReuses {};
88 };
89
90 /// @brief Constructs a cache with the given capacity.
91 /// @param capacity Maximum number of idle prepared handles to keep; @c 0 disables the cache.
92 LIGHTWEIGHT_API explicit SqlPreparedStatementCache(std::size_t capacity = 0) noexcept;
93
94 /// Frees every pooled statement handle.
95 LIGHTWEIGHT_API ~SqlPreparedStatementCache() noexcept;
96
98 SqlPreparedStatementCache& operator=(SqlPreparedStatementCache const&) = delete;
101
102 /// @return The maximum number of idle prepared handles kept (@c 0 when disabled).
103 [[nodiscard]] std::size_t Capacity() const noexcept
104 {
105 return m_capacity;
106 }
107
108 /// @brief Sets the capacity, evicting the least recently released handles when shrinking.
109 /// @param capacity Maximum number of idle prepared handles to keep; @c 0 disables and clears.
110 LIGHTWEIGHT_API void SetCapacity(std::size_t capacity) noexcept;
111
112 /// @return Whether the cache is enabled, i.e. whether its capacity is non-zero.
113 [[nodiscard]] bool IsEnabled() const noexcept
114 {
115 return m_capacity != 0;
116 }
117
118 /// @return The number of idle prepared handles currently pooled.
119 [[nodiscard]] std::size_t Size() const noexcept
120 {
121 return m_entries.size();
122 }
123
124 /// @return The cumulative hit/miss/eviction counters.
125 [[nodiscard]] Statistics const& Stats() const noexcept
126 {
127 return m_stats;
128 }
129
130 /// Resets the cumulative counters to zero, leaving the pooled handles untouched.
131 LIGHTWEIGHT_API void ResetStatistics() noexcept;
132
133 /// Counts a prepare that reused the statement's own handle instead of the pool.
134 /// @see Statistics::directReuses
135 void RecordDirectReuse() noexcept
136 {
137 ++m_stats.directReuses;
138 }
139
140 /// @brief Takes an idle handle prepared for @p query out of the pool.
141 ///
142 /// The caller owns the returned handle until it hands it back via @ref Release (or frees it).
143 ///
144 /// @param query The exact SQL text the handle must have been prepared with.
145 /// @return The pooled handle, or @c std::nullopt when no idle handle matches.
146 [[nodiscard]] LIGHTWEIGHT_API std::optional<PreparedHandle> Acquire(std::string_view query) noexcept;
147
148 /// @brief Hands a prepared handle back to the pool as the most recently used entry.
149 ///
150 /// The caller must have closed the handle's cursor and unbound its columns beforehand. Ownership
151 /// of @p handle transfers to the cache; when the capacity bound is exceeded — or the cache is
152 /// disabled — the surplus handle is freed right away.
153 ///
154 /// @param query The SQL text @p handle is prepared for.
155 /// @param handle The prepared handle to pool.
156 LIGHTWEIGHT_API void Release(std::string_view query, PreparedHandle handle) noexcept;
157
158 /// Frees every pooled handle, e.g. after DDL invalidated the cached query plans.
159 LIGHTWEIGHT_API void Clear() noexcept;
160
161 private:
162 /// One pooled handle plus the query text it is keyed by. Held in a list so node addresses — and
163 /// therefore the @c string_view keys of @c m_index, which point into @c query — stay stable.
164 struct Entry
165 {
166 std::string query;
167 PreparedHandle handle;
168 };
169
170 using EntryList = std::list<Entry>;
171
172 /// Drops the index entry referring to @p entry (there may be several entries per query text).
173 void EraseFromIndex(EntryList::const_iterator entry) noexcept;
174
175 /// Frees the least recently released handles until at most @c m_capacity remain.
176 void EvictSurplus() noexcept;
177
178 std::size_t m_capacity;
179 EntryList m_entries; // front = most recently used
180 std::unordered_multimap<std::string_view, EntryList::iterator> m_index; // query text -> entry
181 Statistics m_stats {};
182};
183
184} // namespace Lightweight
A bounded LRU pool of already-prepared ODBC statement handles, owned by a SqlConnection.
LIGHTWEIGHT_API std::optional< PreparedHandle > Acquire(std::string_view query) noexcept
Takes an idle handle prepared for query out of the pool.
Statistics const & Stats() const noexcept
LIGHTWEIGHT_API void SetCapacity(std::size_t capacity) noexcept
Sets the capacity, evicting the least recently released handles when shrinking.
LIGHTWEIGHT_API void ResetStatistics() noexcept
Resets the cumulative counters to zero, leaving the pooled handles untouched.
LIGHTWEIGHT_API SqlPreparedStatementCache(std::size_t capacity=0) noexcept
Constructs a cache with the given capacity.
LIGHTWEIGHT_API void Release(std::string_view query, PreparedHandle handle) noexcept
Hands a prepared handle back to the pool as the most recently used entry.
LIGHTWEIGHT_API void Clear() noexcept
Frees every pooled handle, e.g. after DDL invalidated the cached query plans.
SqlPreparedStatementCaching
Whether a single SqlStatement takes part in its connection's prepared-statement cache.
@ Enabled
Request an encrypted connection (SQL_EN_ON).
@ Disabled
Request an unencrypted connection (SQL_EN_OFF).
A pooled statement handle together with the parameter count the driver reported for it.
SQLHSTMT nativeHandle
The native ODBC statement handle, prepared for the associated query text.
SQLSMALLINT parameterCount
The number of input parameters SQLNumParams reported for that query.
Cumulative counters, primarily for tests and diagnostics.
uint64_t misses
Prepare requests that had to issue SQLPrepare.
uint64_t evictions
Pooled handles freed because the capacity bound was exceeded.
uint64_t hits
Prepare requests served from the pool, i.e. reusing a handle the server already prepared.