diff options
| author | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
| commit | 52b3cd46c76fd4be18aeb8422a08a073484b9fad (patch) | |
| tree | b4f23957c096e6bce5369bb94a4e8e23de71761a /server/src/sqlite3.cppm | |
| parent | 35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff) | |
| download | routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip | |
More module implementation partition units
Diffstat (limited to 'server/src/sqlite3.cppm')
| -rw-r--r-- | server/src/sqlite3.cppm | 248 |
1 files changed, 50 insertions, 198 deletions
diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm index 4623cdc..b771c49 100644 --- a/server/src/sqlite3.cppm +++ b/server/src/sqlite3.cppm | |||
| @@ -1,20 +1,34 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <sqlite3.h> | ||
| 4 | |||
| 5 | export module routemon:sqlite3; | 1 | export module routemon:sqlite3; |
| 6 | 2 | ||
| 7 | import std; | 3 | import std; |
| 8 | import :util; | 4 | import :util; |
| 9 | 5 | ||
| 6 | extern "C" | ||
| 7 | { | ||
| 8 | using sqlite3 = struct sqlite3; | ||
| 9 | using sqlite3_mutex = struct sqlite3_mutex; | ||
| 10 | using sqlite3_stmt = struct sqlite3_stmt; | ||
| 11 | } | ||
| 12 | |||
| 10 | namespace routemon::sqlite3 { | 13 | namespace routemon::sqlite3 { |
| 11 | 14 | ||
| 15 | template <class T, template <class U> concept C> | ||
| 16 | concept optional_of = requires { | ||
| 17 | typename T::value_type; | ||
| 18 | requires std::same_as<T, std::optional<typename T::value_type>>; | ||
| 19 | requires C<typename T::value_type>; | ||
| 20 | }; | ||
| 21 | |||
| 22 | template <class T> | ||
| 23 | concept scannable_prim = std::same_as<T, std::string> || std::same_as<T, double> | ||
| 24 | || std::same_as<T, std::int64_t>; | ||
| 25 | |||
| 26 | template <class T> | ||
| 27 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | ||
| 28 | |||
| 12 | class mutex_guard | 29 | class mutex_guard |
| 13 | { | 30 | { |
| 14 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} | 31 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept; |
| 15 | { | ||
| 16 | ::sqlite3_mutex_enter(mut_); | ||
| 17 | } | ||
| 18 | 32 | ||
| 19 | friend auto | 33 | friend auto |
| 20 | do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | 34 | do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) |
| @@ -22,111 +36,54 @@ class mutex_guard | |||
| 22 | 36 | ||
| 23 | public: | 37 | public: |
| 24 | mutex_guard(mutex_guard const&) = delete; | 38 | mutex_guard(mutex_guard const&) = delete; |
| 25 | ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } | 39 | ~mutex_guard(); |
| 26 | 40 | ||
| 27 | private: | 41 | private: |
| 28 | ::sqlite3_mutex* mut_; | 42 | ::sqlite3_mutex* mut_; |
| 29 | }; | 43 | }; |
| 30 | 44 | ||
| 31 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | ||
| 32 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 33 | { | ||
| 34 | return f(mutex_guard{mut}); | ||
| 35 | } | ||
| 36 | |||
| 37 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) | ||
| 38 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 39 | { | ||
| 40 | return do_guarded(::sqlite3_db_mutex(dbc), f); | ||
| 41 | } | ||
| 42 | |||
| 43 | class error : public std::exception | 45 | class error : public std::exception |
| 44 | { | 46 | { |
| 45 | int code_; | 47 | int code_; |
| 46 | std::string message_; | 48 | std::string message_; |
| 47 | 49 | ||
| 48 | public: | 50 | public: |
| 49 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc) | 51 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc); |
| 50 | : code_{code}, message_{::sqlite3_errmsg(dbc)} | 52 | explicit error(int code); |
| 51 | { | ||
| 52 | } | ||
| 53 | |||
| 54 | explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} | ||
| 55 | |||
| 56 | [[nodiscard]] auto what() const noexcept -> char const* override | ||
| 57 | { | ||
| 58 | return message_.c_str(); | ||
| 59 | } | ||
| 60 | 53 | ||
| 61 | [[nodiscard]] auto code() const noexcept -> int { return code_; } | 54 | [[nodiscard]] auto what() const noexcept -> char const* override; |
| 62 | }; | 55 | [[nodiscard]] auto code() const noexcept -> int; |
| 63 | |||
| 64 | template <class T, template <class U> concept C> | ||
| 65 | concept optional_of = requires { | ||
| 66 | typename T::value_type; | ||
| 67 | requires std::same_as<T, std::optional<typename T::value_type>>; | ||
| 68 | requires C<typename T::value_type>; | ||
| 69 | }; | 56 | }; |
| 70 | 57 | ||
| 71 | template <class T> | ||
| 72 | concept scannable_prim = std::same_as<T, std::string> || std::same_as<T, double> | ||
| 73 | || std::same_as<T, std::int64_t>; | ||
| 74 | |||
| 75 | template <class T> | ||
| 76 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | ||
| 77 | |||
| 78 | class statement | 58 | class statement |
| 79 | { | 59 | { |
| 80 | ::sqlite3_stmt* stmt_; | 60 | ::sqlite3_stmt* stmt_; |
| 81 | 61 | ||
| 82 | public: | 62 | public: |
| 83 | explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} | 63 | explicit statement(::sqlite3_stmt* stmt); |
| 84 | statement(statement const&) = delete; | 64 | statement(statement const&) = delete; |
| 85 | statement(statement&& s) noexcept | 65 | statement(statement&& s) noexcept; |
| 86 | { | 66 | ~statement(); |
| 87 | stmt_ = s.stmt_; | 67 | auto get() -> ::sqlite3_stmt*; |
| 88 | s.stmt_ = nullptr; | ||
| 89 | } | ||
| 90 | ~statement() { ::sqlite3_finalize(stmt_); } | ||
| 91 | auto get() -> ::sqlite3_stmt* { return stmt_; } | ||
| 92 | }; | 68 | }; |
| 93 | 69 | ||
| 94 | class row_reader | 70 | class row_reader |
| 95 | { | 71 | { |
| 96 | statement stmt_; | 72 | statement stmt_; |
| 97 | 73 | ||
| 98 | explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} | 74 | explicit row_reader(statement stmt); |
| 99 | 75 | ||
| 100 | friend class connection; | 76 | friend class connection; |
| 101 | 77 | ||
| 102 | void scan(int col, std::string& s) | 78 | auto is_null(int col) -> bool; |
| 103 | { | 79 | auto ncols() -> std::size_t; |
| 104 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) | ||
| 105 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 106 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); | ||
| 107 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); | ||
| 108 | if (!size.has_value()) | ||
| 109 | throw std::logic_error{"unexpected negative amount of bytes in column"}; | ||
| 110 | s = std::string{reinterpret_cast<char const*>(chs), *size}; | ||
| 111 | } | ||
| 112 | 80 | ||
| 113 | void scan(int col, double& v) | 81 | auto scan(int col, std::string& s) -> void; |
| 82 | auto scan(int col, double& v) -> void; | ||
| 83 | auto scan(int col, std::int64_t& v) -> void; | ||
| 84 | auto scan(int col, optional_of<scannable> auto& v) -> void | ||
| 114 | { | 85 | { |
| 115 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) | 86 | if (is_null(col)) |
| 116 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 117 | v = ::sqlite3_column_double(stmt_.get(), col); | ||
| 118 | } | ||
| 119 | |||
| 120 | void scan(int col, std::int64_t& v) | ||
| 121 | { | ||
| 122 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) | ||
| 123 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 124 | v = ::sqlite3_column_int64(stmt_.get(), col); | ||
| 125 | } | ||
| 126 | |||
| 127 | void scan(int col, optional_of<scannable> auto& v) | ||
| 128 | { | ||
| 129 | if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) | ||
| 130 | { | 87 | { |
| 131 | v.reset(); | 88 | v.reset(); |
| 132 | } | 89 | } |
| @@ -139,28 +96,11 @@ class row_reader | |||
| 139 | } | 96 | } |
| 140 | 97 | ||
| 141 | public: | 98 | public: |
| 142 | auto next() -> bool | 99 | auto next() -> bool; |
| 143 | { | ||
| 144 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); | ||
| 145 | return do_guarded( | ||
| 146 | dbc, | ||
| 147 | [&](auto const& guard) -> bool | ||
| 148 | { | ||
| 149 | auto const s = ::sqlite3_step(stmt_.get()); | ||
| 150 | if (s == SQLITE_ROW) | ||
| 151 | return true; | ||
| 152 | if (s == SQLITE_DONE) | ||
| 153 | return false; | ||
| 154 | throw error{guard, s, dbc}; | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | 100 | ||
| 158 | auto scan(scannable auto&... args) -> void | 101 | auto scan(scannable auto&... args) -> void |
| 159 | { | 102 | { |
| 160 | auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | 103 | if (sizeof...(args) > ncols()) |
| 161 | if (!ncols.has_value()) | ||
| 162 | throw std::logic_error{"got unexpected negative amount of columns"}; | ||
| 163 | if (sizeof...(args) > *ncols) | ||
| 164 | throw std::invalid_argument{ | 104 | throw std::invalid_argument{ |
| 165 | "more scanning arguments provided than columns in result set" | 105 | "more scanning arguments provided than columns in result set" |
| 166 | }; | 106 | }; |
| @@ -184,30 +124,14 @@ class binder | |||
| 184 | { | 124 | { |
| 185 | statement& stmt_; | 125 | statement& stmt_; |
| 186 | 126 | ||
| 187 | explicit binder(statement& stmt) : stmt_{stmt} {} | 127 | explicit binder(statement& stmt); |
| 188 | 128 | ||
| 189 | friend class connection; | 129 | friend class connection; |
| 190 | 130 | ||
| 191 | public: | 131 | public: |
| 192 | auto text(std::string const& param_name, std::string_view str) -> void | 132 | auto text(std::string const& param_name, std::string_view str) -> void; |
| 193 | { | ||
| 194 | int const i = | ||
| 195 | ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); | ||
| 196 | if (i == 0) | ||
| 197 | throw std::invalid_argument{std::format( | ||
| 198 | "bind: no parameter with name {} found", param_name)}; | ||
| 199 | auto str_size = util::int_from_size(str.size()); | ||
| 200 | if (!str_size.has_value()) | ||
| 201 | throw std::invalid_argument{"bind: provided text is too long"}; | ||
| 202 | if (auto s = ::sqlite3_bind_text( | ||
| 203 | stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); | ||
| 204 | s != SQLITE_OK) | ||
| 205 | { | ||
| 206 | throw error{s}; | ||
| 207 | } | ||
| 208 | } | ||
| 209 | 133 | ||
| 210 | static auto noop(binder&) -> void {} | 134 | static auto noop(binder&) -> void; |
| 211 | }; | 135 | }; |
| 212 | 136 | ||
| 213 | export class connection | 137 | export class connection |
| @@ -215,97 +139,25 @@ export class connection | |||
| 215 | ::sqlite3* dbc_; | 139 | ::sqlite3* dbc_; |
| 216 | ::sqlite3_mutex* mut_; | 140 | ::sqlite3_mutex* mut_; |
| 217 | 141 | ||
| 218 | explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} | 142 | explicit connection(::sqlite3* dbc); |
| 219 | { | ||
| 220 | } | ||
| 221 | 143 | ||
| 222 | friend auto open(std::string const& filename) -> connection; | 144 | friend auto open(std::string const& filename) -> connection; |
| 223 | 145 | ||
| 224 | public: | 146 | public: |
| 225 | connection(connection const&) = delete; | 147 | connection(connection const&) = delete; |
| 226 | connection(connection&& c) noexcept | 148 | connection(connection&& c) noexcept; |
| 227 | { | ||
| 228 | dbc_ = c.dbc_; | ||
| 229 | mut_ = c.mut_; | ||
| 230 | c.dbc_ = nullptr; | ||
| 231 | c.mut_ = nullptr; | ||
| 232 | } | ||
| 233 | 149 | ||
| 234 | [[nodiscard]] auto query( | 150 | [[nodiscard]] auto query( |
| 235 | std::string const& sql, | 151 | std::string const& sql, |
| 236 | std::function<void(binder&)> const& bf = binder::noop) -> row_reader | 152 | std::function<void(binder&)> const& bf = binder::noop) -> row_reader; |
| 237 | { | ||
| 238 | ::sqlite3_stmt* pstmt = nullptr; | ||
| 239 | char const* sql_tail = nullptr; | ||
| 240 | auto sql_size = util::int_from_size(sql.size()); | ||
| 241 | if (!sql_size.has_value() | ||
| 242 | || *sql_size >= std::numeric_limits<int>::max() - 1) | ||
| 243 | throw std::invalid_argument{"provided input text too large"}; | ||
| 244 | do_guarded( | ||
| 245 | mut_, | ||
| 246 | [&](auto const& guard) -> void | ||
| 247 | { | ||
| 248 | if (auto s = ::sqlite3_prepare_v2( | ||
| 249 | dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); | ||
| 250 | s != SQLITE_OK) | ||
| 251 | { | ||
| 252 | if (pstmt != nullptr) | ||
| 253 | { | ||
| 254 | // Use contract_assert when having a compiler with | ||
| 255 | // contracts available | ||
| 256 | ::sqlite3_finalize(pstmt); | ||
| 257 | throw std::logic_error{ | ||
| 258 | "expected stmt to be null after failed preparation" | ||
| 259 | }; | ||
| 260 | } | ||
| 261 | throw error{guard, s, dbc_}; | ||
| 262 | } | ||
| 263 | }); | ||
| 264 | if (!pstmt) | ||
| 265 | throw std::invalid_argument{"provided input text contains no SQL"}; | ||
| 266 | auto stmt = statement{pstmt}; | ||
| 267 | if (sql_tail && std::strlen(sql_tail) > 0) | ||
| 268 | throw std::invalid_argument{ | ||
| 269 | "provided input text contains more than one SQL statement" | ||
| 270 | }; | ||
| 271 | auto b = binder{stmt}; | ||
| 272 | bf(b); | ||
| 273 | return row_reader{std::move(stmt)}; | ||
| 274 | } | ||
| 275 | 153 | ||
| 276 | auto exec( | 154 | auto exec( |
| 277 | std::string const& sql, | 155 | std::string const& sql, |
| 278 | std::function<void(binder&)> const& bf = binder::noop) -> void | 156 | std::function<void(binder&)> const& bf = binder::noop) -> void; |
| 279 | { | ||
| 280 | auto reader = query(sql, bf); | ||
| 281 | while (reader.next()) | ||
| 282 | ; | ||
| 283 | } | ||
| 284 | 157 | ||
| 285 | ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } | 158 | ~connection(); |
| 286 | }; | 159 | }; |
| 287 | 160 | ||
| 288 | export auto open(std::string const& filename) -> connection | 161 | export auto open(std::string const& filename) -> connection; |
| 289 | { | ||
| 290 | ::sqlite3* dbc = nullptr; | ||
| 291 | auto s = ::sqlite3_open_v2( | ||
| 292 | filename.c_str(), &dbc, | ||
| 293 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX | ||
| 294 | | SQLITE_OPEN_EXRESCODE, | ||
| 295 | nullptr); | ||
| 296 | if (s != SQLITE_OK) | ||
| 297 | { | ||
| 298 | if (dbc) | ||
| 299 | { | ||
| 300 | do_guarded( | ||
| 301 | dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); | ||
| 302 | } | ||
| 303 | else | ||
| 304 | { | ||
| 305 | throw error{s}; | ||
| 306 | } | ||
| 307 | } | ||
| 308 | return connection{dbc}; | ||
| 309 | } | ||
| 310 | 162 | ||
| 311 | } // namespace routemon::sqlite3 | 163 | } // namespace routemon::sqlite3 |