diff options
| author | Rutger Broekhoff | 2026-08-28 18:03:05 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-08-28 18:03:05 +0200 |
| commit | 973aec43ea54bbf95b64fbcb636403401d1ca60e (patch) | |
| tree | 41b7911c420766a9b463245b9296f44c5bf35258 /server/src/sqlite3.cppm | |
| download | routemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.tar.gz routemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.zip | |
Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14
Diffstat (limited to 'server/src/sqlite3.cppm')
| -rw-r--r-- | server/src/sqlite3.cppm | 257 |
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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <sqlite3.h> | ||
| 4 | |||
| 5 | export module routemon:sqlite3; | ||
| 6 | |||
| 7 | import std; | ||
| 8 | import :util; | ||
| 9 | |||
| 10 | namespace 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 | ||