Lightweight 0.20260522.0
Loading...
Searching...
No Matches
SqlAdvisoryLock.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Api.hpp"
6#include "SqlError.hpp"
7
8#include <chrono>
9#include <cstdint>
10#include <expected>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <vector>
15
16namespace Lightweight
17{
18
19class SqlConnection;
20
21/// Reason an advisory-lock operation failed.
22///
23/// Carried inside `SqlLockError` so callers can decide whether to retry,
24/// escalate, or surface the cause to a human — without resorting to
25/// `what()`-string text matching.
26enum class SqlLockFailureReason : uint8_t
27{
28 /// Lock wasn't acquired within the requested timeout.
29 Timeout,
30 /// Server picked us as the deadlock victim (e.g. SQL Server `sp_getapplock` = -3).
31 Deadlock,
32 /// Lock request cancelled by the server (e.g. SQL Server `sp_getapplock` = -2).
33 Cancelled,
34 /// Backend reported a parameter or validation error (e.g. SQL Server `sp_getapplock` = -999).
35 ParameterError,
36 /// Any other driver/server-level failure; `SqlLockError::info` carries the SQLSTATE detail.
37 DriverError,
38};
39
40/// Structured error returned by `SqlAdvisoryLockHandler::TryAcquire` and
41/// `SqlAdvisoryLockHandler::Release`.
42///
43/// Lets callers propagate the *root cause* of a lock failure (timeout vs.
44/// deadlock vs. driver-level fault) instead of having to text-match an
45/// exception message.
47{
48 /// What went wrong, at the granularity callers actually care about.
49 SqlLockFailureReason reason {};
50 /// The lock name that was being acquired or released — handy when several
51 /// distinct locks coexist in a process.
52 std::string lockName;
53 /// Requested timeout (zero for `Release`).
54 std::chrono::milliseconds timeout {};
55 /// Pre-formatted, human-readable explanation. Already includes the lock
56 /// name and timeout where relevant, so callers can `std::println("{}", err.message)`
57 /// without further formatting.
58 std::string message;
59 /// Set on `DriverError`; carries SQLSTATE / native error / driver text.
60 std::optional<SqlErrorInfo> info;
61};
62
63/// Dialect-specific implementation of advisory-lock acquire/release.
64///
65/// Each `SqlQueryFormatter` returns a process-singleton instance of the
66/// appropriate concrete handler via `SqlQueryFormatter::AdvisoryLockOps()`.
67/// Concrete implementations live next to the formatter that owns them
68/// (e.g. the SQL Server handler is defined alongside `SqlServerQueryFormatter`),
69/// keeping every dialect quirk in one source file and out of the
70/// `SqlScopedLock` business logic.
71///
72/// Callers that need a lock should use the `SqlScopedLock` RAII wrapper —
73/// this type is the extension point for adding new dialects.
74///
75/// Naming: these are *advisory* locks (also called application or named
76/// locks): they don't lock any particular row or table; they're purely
77/// cooperative tokens identified by a string name. Two processes that
78/// agree to acquire the same name serialise on it.
79class [[nodiscard]] LIGHTWEIGHT_API SqlAdvisoryLockHandler
80{
81 public:
82 SqlAdvisoryLockHandler() = default;
83 /// Polymorphic destructor.
84 virtual ~SqlAdvisoryLockHandler() = default;
85
87 SqlAdvisoryLockHandler& operator=(SqlAdvisoryLockHandler const&) = delete;
90
91 /// Attempts to acquire the named lock on `connection`, blocking up to `timeout` ms.
92 ///
93 /// Returns an empty `expected` on success; otherwise a fully-populated
94 /// `SqlLockError` so the caller can distinguish timeout from deadlock
95 /// from driver error and propagate accordingly.
96 ///
97 /// Implementations MUST NOT throw on a recoverable timeout — that's a
98 /// `SqlLockFailureReason::Timeout`. They MAY throw only on truly
99 /// catastrophic conditions (out-of-memory, ABI mismatch); routine driver
100 /// failures should populate `SqlLockFailureReason::DriverError` with
101 /// `info` set.
102 ///
103 /// @param connection The SQL connection to acquire the lock on.
104 /// @param lockName The name of the lock (advisory; backends hash or quote as needed).
105 /// @param timeout Maximum time to wait for lock acquisition.
106 /// @return Empty `expected` on success, populated `SqlLockError` on failure.
107 [[nodiscard]] virtual std::expected<void, SqlLockError> TryAcquire(SqlConnection& connection,
108 std::string_view lockName,
109 std::chrono::milliseconds timeout) const = 0;
110
111 /// Releases the named lock previously acquired with `TryAcquire`.
112 ///
113 /// Implementations MUST tolerate "release of a lock that's already gone"
114 /// (connection teardown, server-side session expiry, etc.) as success —
115 /// `Release` is idempotent by contract.
116 ///
117 /// Returns an error only if the release round-trip itself failed. The
118 /// caller (typically `SqlScopedLock`'s destructor) should log such an
119 /// error rather than swallow it silently.
120 ///
121 /// @param connection The SQL connection that holds the lock.
122 /// @param lockName The name of the lock to release.
123 /// @return Empty `expected` on success, populated `SqlLockError` on failure.
124 [[nodiscard]] virtual std::expected<void, SqlLockError> Release(SqlConnection& connection,
125 std::string_view lockName) const = 0;
126
127 /// Names of any backend-internal bookkeeping tables this handler
128 /// maintains — empty for backends with server-native advisory locks
129 /// (SQL Server's `sp_getapplock`, PostgreSQL's `pg_advisory_lock`),
130 /// non-empty for backends that implement locking via a regular table
131 /// (SQLite returns its lock-table name here).
132 ///
133 /// Tooling that walks the live schema (`dbtool hard-reset`, schema
134 /// diffs, backups that try to skip internal tables) consults this so
135 /// the lock table is recognised as infrastructure rather than mistaken
136 /// for user data.
137 [[nodiscard]] virtual std::vector<std::string_view> BookkeepingTableNames() const noexcept
138 {
139 return {};
140 }
141};
142
143/// @brief Returns the SQLite-specific singleton handler.
144///
145/// Defined in `SqlQueryFormatter.cpp` so the lock execution code (which needs
146/// `SqlConnection` / `SqlStatement` includes) doesn't bleed into the
147/// formatter headers. The formatter overrides delegate to these free
148/// functions inline, which keeps every formatter's vtable weak — the same
149/// emission shape as before this refactor, so the shared-library build
150/// doesn't need a class-level `LIGHTWEIGHT_API` retrofitted onto the
151/// concrete formatter types.
152[[nodiscard]] LIGHTWEIGHT_API SqlAdvisoryLockHandler const& SqliteAdvisoryLockOps();
153
154/// @brief Returns the SQL Server-specific singleton handler. See `SqliteAdvisoryLockOps()`.
155[[nodiscard]] LIGHTWEIGHT_API SqlAdvisoryLockHandler const& SqlServerAdvisoryLockOps();
156
157/// @brief Returns the PostgreSQL-specific singleton handler. See `SqliteAdvisoryLockOps()`.
158[[nodiscard]] LIGHTWEIGHT_API SqlAdvisoryLockHandler const& PostgreSqlAdvisoryLockOps();
159
160} // namespace Lightweight
virtual std::expected< void, SqlLockError > Release(SqlConnection &connection, std::string_view lockName) const =0
virtual std::vector< std::string_view > BookkeepingTableNames() const noexcept
virtual std::expected< void, SqlLockError > TryAcquire(SqlConnection &connection, std::string_view lockName, std::chrono::milliseconds timeout) const =0
virtual ~SqlAdvisoryLockHandler()=default
Polymorphic destructor.
Represents a connection to a SQL database.
SqlLockFailureReason reason
What went wrong, at the granularity callers actually care about.
std::chrono::milliseconds timeout
Requested timeout (zero for Release).
std::optional< SqlErrorInfo > info
Set on DriverError; carries SQLSTATE / native error / driver text.