summaryrefslogtreecommitdiffstats
path: root/server/src/sqlite3.cppm
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/sqlite3.cppm')
-rw-r--r--server/src/sqlite3.cppm257
1 files changed, 257 insertions, 0 deletions
diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm
new file mode 100644
index 0000000..f226c6b
--- /dev/null
+++ b/server/src/sqlite3.cppm
@@ -0,0 +1,257 @@
1module;
2
3#include <sqlite3.h>
4
5export module routemon:sqlite3;
6
7import std;
8import :util;
9
10namespace routemon::sqlite3 {
11
12 class mutex_guard {
13 explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} {
14 ::sqlite3_mutex_enter(mut_);
15 }
16
17 friend auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>()));
18
19 public:
20 mutex_guard(mutex_guard const&) = delete;
21 ~mutex_guard() {
22 ::sqlite3_mutex_leave(mut_);
23 }
24
25 private:
26 ::sqlite3_mutex* mut_;
27 };
28
29 auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) {
30 return f(mutex_guard{mut});
31 }
32
33 auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) {
34 return do_guarded(::sqlite3_db_mutex(dbc), f);
35 }
36
37 class error : public std::exception {
38 int code_;
39 std::string message_;
40
41 public:
42 explicit error(mutex_guard const&, int code, ::sqlite3* dbc)
43 : code_{code}, message_{::sqlite3_errmsg(dbc)}
44 {}
45
46 explicit error(int code)
47 : code_{code}, message_{::sqlite3_errstr(code)}
48 {}
49
50 [[nodiscard]] auto what() const noexcept -> char const* override {
51 return message_.c_str();
52 }
53
54 [[nodiscard]] auto code() const noexcept -> int {
55 return code_;
56 }
57 };
58
59 template<class T, template<class U> concept C>
60 concept optional_of = requires {
61 typename T::value_type;
62 requires std::same_as<T, std::optional<typename T::value_type>>;
63 requires C<typename T::value_type>;
64 };
65
66 template<class T>
67 concept scannable_prim =
68 std::same_as<T, std::string> ||
69 std::same_as<T, double> ||
70 std::same_as<T, std::int64_t>;
71
72 template<class T>
73 concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>;
74
75 class statement {
76 ::sqlite3_stmt* stmt_;
77
78 public:
79 explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {}
80 statement(statement const&) = delete;
81 statement(statement&& s) noexcept {
82 stmt_ = s.stmt_;
83 s.stmt_ = nullptr;
84 }
85 ~statement() {
86 ::sqlite3_finalize(stmt_);
87 }
88 auto get() -> ::sqlite3_stmt* {
89 return stmt_;
90 }
91 };
92
93 class row_reader {
94 statement stmt_;
95
96 explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {}
97
98 friend class connection;
99
100 void scan(int col, std::string& s) {
101 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT)
102 throw std::invalid_argument{"invalid type for scan"};
103 unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col);
104 auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col));
105 if (!size.has_value())
106 throw std::logic_error{"unexpected negative amount of bytes in column"};
107 s = std::string{reinterpret_cast<char const*>(chs), *size};
108 }
109
110 void scan(int col, double& v) {
111 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT)
112 throw std::invalid_argument{"invalid type for scan"};
113 v = ::sqlite3_column_double(stmt_.get(), col);
114 }
115
116 void scan(int col, std::int64_t& v) {
117 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER)
118 throw std::invalid_argument{"invalid type for scan"};
119 v = ::sqlite3_column_int64(stmt_.get(), col);
120 }
121
122 void scan(int col, optional_of<scannable> auto& v) {
123 if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) {
124 v.reset();
125 } else {
126 typename std::remove_cvref_t<decltype(v)>::value_type tmp;
127 scan(col, tmp);
128 v = std::move(tmp);
129 }
130 }
131
132 public:
133 auto next() -> bool {
134 ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get());
135 return do_guarded(dbc, [&](auto const& guard) -> bool {
136 auto const s = ::sqlite3_step(stmt_.get());
137 if (s == SQLITE_ROW)
138 return true;
139 if (s == SQLITE_DONE)
140 return false;
141 throw error{guard, s, dbc};
142 });
143 }
144
145 auto scan(scannable auto&... args) -> void {
146 auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get()));
147 if (!ncols.has_value())
148 throw std::logic_error{"got unexpected negative amount of columns"};
149 if (sizeof...(args) > *ncols)
150 throw std::invalid_argument{"more scanning arguments provided than columns in result set"};
151 auto col = 0; (..., scan(col++, args));
152 }
153
154 auto scan_single(scannable auto&... args) -> void {
155 if (!next())
156 throw std::logic_error{"no row in result set"};
157 scan(args...);
158 if (next()) {
159 throw std::logic_error{"more than one row in result set"};
160 }
161 }
162 };
163
164 class binder {
165 statement& stmt_;
166
167 explicit binder(statement& stmt) : stmt_{stmt} {}
168
169 friend class connection;
170
171 public:
172 auto text(std::string const& param_name, std::string_view str) -> void {
173 int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str());
174 if (i == 0)
175 throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)};
176 auto str_size = util::int_from_size(str.size());
177 if (!str_size.has_value())
178 throw std::invalid_argument{"bind: provided text is too long"};
179 if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) {
180 throw error{s};
181 }
182 }
183
184 static auto noop(binder&) -> void {}
185 };
186
187 export class connection {
188 ::sqlite3* dbc_;
189 ::sqlite3_mutex* mut_;
190
191 explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {}
192
193 friend auto open(std::string const& filename) -> connection;
194
195 public:
196 connection(connection const&) = delete;
197 connection(connection&& c) noexcept {
198 dbc_ = c.dbc_;
199 mut_ = c.mut_;
200 c.dbc_ = nullptr;
201 c.mut_ = nullptr;
202 }
203
204 [[nodiscard]] auto query(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> row_reader {
205 ::sqlite3_stmt* pstmt = nullptr;
206 char const* sql_tail = nullptr;
207 auto sql_size = util::int_from_size(sql.size());
208 if (!sql_size.has_value() || *sql_size >= std::numeric_limits<int>::max() - 1)
209 throw std::invalid_argument{"provided input text too large"};
210 do_guarded(mut_, [&](auto const& guard) -> void {
211 if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) {
212 if (pstmt != nullptr) {
213 // Use contract_assert when having a compiler with contracts available
214 ::sqlite3_finalize(pstmt);
215 throw std::logic_error{"expected stmt to be null after failed preparation"};
216 }
217 throw error{guard, s, dbc_};
218 }
219 });
220 if (!pstmt)
221 throw std::invalid_argument{"provided input text contains no SQL"};
222 auto stmt = statement{pstmt};
223 if (sql_tail && std::strlen(sql_tail) > 0)
224 throw std::invalid_argument{"provided input text contains more than one SQL statement"};
225 auto b = binder{stmt}; bf(b);
226 return row_reader{std::move(stmt)};
227 }
228
229 auto exec(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> void {
230 auto reader = query(sql, bf);
231 while (reader.next());
232 }
233
234 ~connection() {
235 std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr));
236 }
237 };
238
239 export auto open(std::string const& filename) -> connection {
240 ::sqlite3* dbc = nullptr;
241 auto s = ::sqlite3_open_v2(filename.c_str(), &dbc,
242 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
243 SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE,
244 nullptr);
245 if (s != SQLITE_OK) {
246 if (dbc) {
247 do_guarded(dbc, [&](auto const& guard) -> void {
248 throw error{guard, s, dbc};
249 });
250 } else {
251 throw error{s};
252 }
253 }
254 return connection{dbc};
255 }
256
257} // namespace routemon::sqlite3