include/simdb/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
57 / 70
81.4%
Branches:
39 / 69
56.5%
Functions:
14 / 15
93.3%

sqlite/Transaction.hpp
Line Branch Exec Source
1 // <Transaction.hpp> -*- C++ -*-
2
3 #pragma once
4
5 #include "simdb/Assert.hpp"
6 #include "simdb/Exceptions.hpp"
7
8 #include <chrono>
9 #include <functional>
10 #include <memory>
11 #include <mutex>
12 #include <sqlite3.h>
13 #include <thread>
14
15 namespace simdb {
16
17 /// To support SimDB self-profiling, return TRUE only if the transaction
18 /// involved touching the database (setProperty*(), INSERT, SELECT, etc.)
19 using TransactionFunc = std::function<void()>;
20
21 /*!
22 * \class SQLiteReturnCode
23 * \brief This class wraps a return code and throws a
24 * SafeTransactionSilentException when it encounters a "SQL locked" return code.
25 */
26 class SQLiteReturnCode
27 {
28 public:
29 1400364 explicit SQLiteReturnCode(const int rc) :
30 1400364 rc_(rc)
31 {
32
4/6
✓ Branch 2 → 3 taken 1400363 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 1400363 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 1400363 times.
1400364 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY)
33 {
34
1/1
✓ Branch 6 → 7 taken 1 time.
1 throw SafeTransactionSilentException(rc);
35 }
36 1400363 }
37
38 8 operator int() const { return rc_; }
39
40 655322 operator bool() const { return rc_ != SQLITE_OK; }
41
42 859886 bool operator==(const int rc) { return rc_ == rc; }
43
44 bool operator!=(const int rc) { return rc_ != rc; }
45
46 private:
47 const int rc_;
48 };
49
50 8 inline bool operator==(const int rc, const SQLiteReturnCode& obj)
51 {
52 8 return static_cast<int>(obj) == rc;
53 }
54
55 ✗ inline std::ostream& operator<<(std::ostream& os, const SQLiteReturnCode& rc)
56 {
57 ✗ os << (int)rc;
58 ✗ return os;
59 }
60
61 /*!
62 * \class SQLitePreparedStatement
63 * \brief This class wraps a sqlite3_stmt* and uses RAII to ensure that
64 * sqlite3_finalize() is called so we don't leak resources.
65 */
66 class SQLitePreparedStatement
67 {
68 public:
69 230186 SQLitePreparedStatement(sqlite3* db_conn, const std::string_view cmd)
70 230186 {
71 230186 sqlite3_stmt* stmt = nullptr;
72
1/1
✓ Branch 3 → 4 taken 230186 times.
230186 auto rc = sqlite3_prepare_v2(db_conn, cmd.data(), -1, &stmt, 0);
73
3/6
✓ Branch 4 → 5 taken 230186 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 230186 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 11 taken 230186 times.
230186 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY)
74 {
75
0/1
✗ Branch 7 → 8 not taken.
✗ sqlite3_finalize(stmt);
76
0/1
✗ Branch 9 → 10 not taken.
✗ throw SafeTransactionSilentException(rc);
77 } else
78 {
79
13/13
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 31 taken 230183 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 27 → 28 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
230192 simdb_assert(stmt, "Invalid prepared statement for cmd: " << cmd);
80 }
81
82 230183 stmt_ = stmt;
83 230183 }
84
85 SQLitePreparedStatement(sqlite3_stmt* stmt) :
86 stmt_(stmt)
87 {
88 }
89
90 218 SQLitePreparedStatement(SQLitePreparedStatement&& other) :
91 218 stmt_(other.stmt_)
92 {
93 218 other.stmt_ = nullptr;
94 218 }
95
96 SQLitePreparedStatement(const SQLitePreparedStatement& other) = delete;
97
98 230401 ~SQLitePreparedStatement()
99 {
100
2/2
✓ Branch 2 → 3 taken 210697 times.
✓ Branch 2 → 4 taken 19704 times.
230401 if (stmt_)
101 {
102 210697 sqlite3_finalize(stmt_);
103 }
104 230401 }
105
106 421157 operator sqlite3_stmt*() const { return stmt_; }
107
108 19486 sqlite3_stmt* release()
109 {
110 19486 auto stmt = stmt_;
111 19486 stmt_ = nullptr;
112 19486 return stmt;
113 }
114
115 private:
116 sqlite3_stmt* stmt_ = nullptr;
117 };
118
119 /*!
120 * \class Transaction
121 *
122 * \brief Base class for Connection. Made into a base class
123 * to make it easier for SimDB to be a header-only library
124 * that avoids cyclic header includes.
125 */
126 class Transaction
127 {
128 public:
129 /// Destructor
130 28 virtual ~Transaction() = default;
131
132 /// Execute the functor inside BEGIN/COMMIT TRANSACTION.
133 221804 void safeTransaction(const TransactionFunc& transaction,
134 uint64_t num_retries = std::numeric_limits<uint64_t>::max())
135 {
136 221804 uint64_t num_tries = 0;
137
138 while (true)
139 {
140
1/1
✓ Branch 3 → 4 taken 221804 times.
221804 std::unique_lock<std::recursive_mutex> lock(mutex_);
141
142 // Check to see if we are already in a transaction, in which
143 // case we simply call the transaction function. We cannot
144 // call "BEGIN TRANSACTION" recursively.
145 //
146 // Note that any exception thrown here (including a
147 // SafeTransactionSilentException) must propagate up to the
148 // outermost safeTransaction() call below -- that call is the
149 // one that issued "BEGIN TRANSACTION" and is the only one
150 // that should roll back and/or retry. Retrying or rolling
151 // back here would act on only part of the outer transaction.
152
2/2
✓ Branch 4 → 5 taken 203178 times.
✓ Branch 4 → 7 taken 18626 times.
221804 if (in_transaction_flag_)
153 {
154
1/1
✓ Branch 5 → 6 taken 203178 times.
203178 transaction();
155 203178 return;
156 }
157
158 // We are the outermost safeTransaction() call. Set the flag
159 // for the duration of BEGIN/COMMIT/ROLLBACK so that any nested
160 // safeTransaction() calls (including ones made indirectly by
161 // the exception paths below) know not to issue their own
162 // BEGIN TRANSACTION. The flag is unconditionally cleared
163 // before this call returns or throws, on every code path.
164 18626 in_transaction_flag_ = true;
165 try
166 {
167
1/1
✓ Branch 7 → 8 taken 18626 times.
18626 executeCommand_("BEGIN TRANSACTION");
168
1/1
✓ Branch 8 → 9 taken 18622 times.
18626 transaction();
169
1/1
✓ Branch 9 → 10 taken 18622 times.
18622 executeCommand_("COMMIT TRANSACTION");
170 18622 in_transaction_flag_ = false;
171 18622 return;
172
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 30 taken 4 times.
4 } catch (const SafeTransactionSilentException&)
173 {
174 ✗ in_transaction_flag_ = false;
175 ✗ rollbackSilently_();
176
0/1
✗ Branch 20 → 21 not taken.
✗ lock.unlock();
177
0/1
✗ Branch 22 → 23 not taken.
✗ std::this_thread::sleep_for(std::chrono::milliseconds(25));
178
179 // Loop around and retry the whole transaction from scratch.
180
0/2
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 28 not taken.
✗ if (++num_tries > num_retries)
181 {
182
0/1
✗ Branch 26 → 27 not taken.
✗ throw DBException("Max number of safeTransaction retries exceeded");
183 }
184 4 } catch (...)
185 {
186 4 in_transaction_flag_ = false;
187 4 rollbackSilently_();
188 4 throw;
189 4 }
190 221804 }
191 }
192
193 /// For debug purposes only.
194 bool isInTransaction() const { return in_transaction_flag_; }
195
196 protected:
197 /// Underlying database connection
198 sqlite3* db_conn_ = nullptr;
199
200 private:
201 /// \brief Flag used in RAII safeTransaction() calls. This is
202 /// needed to we know whether to tell SQL to "BEGIN
203 /// TRANSACTION" or not (i.e. if we're already in the
204 /// middle of another safeTransaction).
205 ///
206 /// This allows users to freely do something like this:
207 ///
208 /// \code
209 /// db_mgr_->safeTransaction([&]() {
210 /// doFoo();
211 /// doBar();
212 /// });
213 /// \endcode
214 ///
215 /// Even if doFoo() and doBar() do the same thing:
216 ///
217 /// \code
218 /// void MyClass::doFoo() {
219 /// db_mgr_->safeTransaction([&](){
220 /// ...
221 /// });
222 /// }
223 ///
224 /// void MyClass::doBar() {
225 /// db_mgr_->safeTransaction([&](){
226 /// ...
227 /// });
228 /// }
229 /// \endcode
230 bool in_transaction_flag_ = false;
231
232 /// Mutex for thread-safe reentrant safeTransaction's.
233 std::recursive_mutex mutex_;
234
235 /// Execute the provided statement against the database connection.
236 /// This will validate the command, and throw if this command is
237 /// disallowed. Used for "BEGIN TRANSACTION" and "COMMIT TRANSACTION"
238 /// -- both of these are run as regular (non-destructor) code so that
239 /// any exception they throw, including SafeTransactionSilentException,
240 /// is always caught by safeTransaction()'s try/catch instead of
241 /// escaping from a noexcept destructor and calling std::terminate().
242 37248 void executeCommand_(const char* cmd)
243 {
244
2/2
✓ Branch 2 → 3 taken 37248 times.
✓ Branch 3 → 4 taken 37248 times.
37248 auto rc = SQLiteReturnCode(sqlite3_exec(db_conn_, cmd, nullptr, nullptr, nullptr));
245
4/8
✓ Branch 5 → 6 taken 37248 times.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 37248 times.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 37248 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 17 taken 37248 times.
37248 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY)
246 {
247
0/1
✗ Branch 15 → 16 not taken.
✗ throw SafeTransactionSilentException(rc);
248 } else
249 {
250
1/13
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 38 taken 37248 times.
✗ Branch 21 → 22 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 31 → 32 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 34 → 35 not taken.
✗ Branch 36 → 37 not taken.
37248 simdb_assert(!rc, sqlite3_errmsg(db_conn_));
251 }
252 37248 }
253
254 /// Best-effort "ROLLBACK TRANSACTION", used to undo a transaction that
255 /// failed partway through (whether from a SafeTransactionSilentException
256 /// that we are about to retry, or any other exception propagating out of
257 /// safeTransaction()). We are already unwinding/handling another error
258 /// here, so failures from the rollback itself are intentionally ignored
259 /// rather than thrown -- throwing here could mask the original exception
260 /// or (if BEGIN TRANSACTION never actually succeeded) fail spuriously.
261 4 void rollbackSilently_() noexcept { sqlite3_exec(db_conn_, "ROLLBACK TRANSACTION", nullptr, nullptr, nullptr); }
262 };
263
264 } // namespace simdb
265