#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace db { template constexpr bool is_cstr = false; template constexpr bool is_cstr = true; template constexpr bool is_cstr = true; template <> constexpr bool is_cstr = true; template <> constexpr bool is_cstr = true; // Simple wrapper class that can be used to bind a blob through the templated binding code below. // E.g. `exec_query(st, 100, 42, blob_binder{data})` binds the third parameter using no-copy blob // binding of the contained data. struct blob_binder { std::string_view data; explicit blob_binder(std::string_view d) : data{d} {} }; // Binds a string_view as a no-copy blob at parameter index i. void bind_blob_ref(SQLite::Statement& st, int i, std::string_view blob) { st.bindNoCopy(i, static_cast(blob.data()), blob.size()); } namespace detail { template void bind_oneshot_single(SQLite::Statement& st, int i, const T& val) { if constexpr (std::is_same_v || is_cstr) st.bindNoCopy(i, val); else if constexpr (std::is_same_v) bind_blob_ref(st, i, val.data); else st.bind(i, val); } template void bind_oneshot(SQLite::Statement& st, std::integer_sequence, const T&... bind) { (bind_oneshot_single(st, Index + 1, bind), ...); } } // namespace detail // Called from exec_query and similar to bind statement parameters for immediate execution. // strings (and c strings) use no-copy binding; integer values are bound by value. You can bind a // blob (by reference, like strings) by passing `blob_binder{data}`. template void bind_oneshot(SQLite::Statement& st, const T&... bind) { detail::bind_oneshot(st, std::make_integer_sequence{}, bind...); } // Executes a query that does not expect results. Optionally binds parameters, if provided. // Returns the number of affected rows; throws on error or if results are returned. template int exec_query(SQLite::Statement& st, const T&... bind) { bind_oneshot(st, bind...); return st.exec(); } // Same as above, but prepares a literal query on the fly for use with queries that are only used // once. template int exec_query(SQLite::Database& db, const char* query, const T&... bind) { SQLite::Statement st{db, query}; return exec_query(st, bind...); } template struct first_type { using type = T; }; template using first_type_t = typename first_type::type; template using type_or_tuple = std::conditional_t, std::tuple>; // Retrieves a single row of values from the current state of a statement (i.e. after a // executeStep() call that is expecting a return value). If `T...` is a single type then this // returns the single T value; if T... has multiple types then you get back a tuple of values. template T get(SQLite::Statement& st) { return static_cast(st.getColumn(0)); } template std::tuple get(SQLite::Statement& st) { return st.getColumns, 2 + sizeof...(Tn)>(); } // Steps a statement to completion that is expected to return at most one row, optionally binding // values into it (if provided). Returns a filled out optional (or optional>) // if a row was retrieved, otherwise a nullopt. Throws if more than one row is retrieved. template std::optional> exec_and_maybe_get(SQLite::Statement& st, const Args&... bind) { int i = 1; (bind_oneshot(st, i, bind), ...); std::optional> result; while (st.executeStep()) { if (result) { MERROR("Expected single-row result, got multiple rows from {}" << st.getQuery()); throw std::runtime_error{"DB error: expected single-row result, got multiple rows"}; } result = get(st); } return result; } // Executes a statement to completion that is expected to return exactly one row, optionally // binding values into it (if provided). Returns a T or std::tuple (depending on whether or // not more than one T is provided) for the row. Throws an exception if no rows or more than one // row are returned. template type_or_tuple exec_and_get(SQLite::Statement& st, const Args&... bind) { auto maybe_result = exec_and_maybe_get(st, bind...); if (!maybe_result) { MERROR("Expected single-row result, got no rows from {}" << st.getQuery()); throw std::runtime_error{"DB error: expected single-row result, got no rows"}; } return *std::move(maybe_result); } // Executes a query to completion, collecting each row into a vector (or vector> if // multiple T are given). Can optionally bind before executing. template std::vector> get_all(SQLite::Statement& st, const Bind&... bind) { int i = 1; (bind_oneshot(st, i, bind), ...); std::vector> results; while (st.executeStep()) results.push_back(get(st)); return results; } // Takes a query prefix and suffix and places ? separated by commas between them // Example: multi_in_query("foo(", 3, ")bar") will return "foo(?,?,?)bar" inline std::string multi_in_query(std::string_view prefix, size_t count, std::string_view suffix) { std::string query; query.reserve(prefix.size() + (count == 0 ? 0 : 2 * count - 1) + suffix.size()); query += prefix; for (size_t i = 0; i < count; i++) { if (i > 0) query += ','; query += '?'; } query += suffix; return query; } // Storage database class. class Database { // SQLiteCpp's statements are not thread-safe, so we prepare them thread-locally when needed std::unordered_map> prepared_sts; std::shared_mutex prepared_sts_mutex; /** Wrapper around a SQLite::Statement that calls `tryReset()` on destruction of the wrapper. */ class StatementWrapper { SQLite::Statement& st; public: /// Whether we should reset on destruction; can be set to false if needed. bool reset_on_destruction = true; explicit StatementWrapper(SQLite::Statement& st) noexcept : st{st} {} ~StatementWrapper() noexcept { if (reset_on_destruction) st.tryReset(); } SQLite::Statement& operator*() noexcept { return st; } SQLite::Statement* operator->() noexcept { return &st; } operator SQLite::Statement&() noexcept { return st; } }; public: SQLite::Database db; StatementWrapper prepared_st(const std::string& query) { std::unordered_map* sts; { std::shared_lock rlock{prepared_sts_mutex}; if (auto it = prepared_sts.find(std::this_thread::get_id()); it != prepared_sts.end()) sts = &it->second; else { rlock.unlock(); std::unique_lock wlock{prepared_sts_mutex}; sts = &prepared_sts.try_emplace(std::this_thread::get_id()).first->second; } } if (auto qit = sts->find(query); qit != sts->end()) return StatementWrapper{qit->second}; return StatementWrapper{sts->try_emplace(query, db, query).first->second}; } template int prepared_exec(const std::string& query, const T&... bind) { return exec_query(prepared_st(query), bind...); } template auto prepared_get(const std::string& query, const Bind&... bind) { return exec_and_get(prepared_st(query), bind...); } template auto prepared_maybe_get(const std::string& query, const Bind&... bind) { return exec_and_maybe_get(prepared_st(query), bind...); } explicit Database(const std::filesystem::path& db_path, const std::string_view db_password) : db{db_path, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE | SQLite::OPEN_FULLMUTEX} { // Don't fail on these because we can still work even if they fail if (int rc = db.tryExec("PRAGMA journal_mode = WAL"); rc != SQLITE_OK) MERROR("Failed to set journal mode to WAL: {}" << sqlite3_errstr(rc)); if (int rc = db.tryExec("PRAGMA synchronous = NORMAL"); rc != SQLITE_OK) MERROR("Failed to set synchronous mode to NORMAL: {}" << sqlite3_errstr(rc)); if (int rc = db.tryExec("PRAGMA foreign_keys = ON"); rc != SQLITE_OK) { auto m = fmt::format("Failed to enable foreign keys constraints: {}", sqlite3_errstr(rc)); MERROR(m); throw std::runtime_error{m}; } int fk_enabled = db.execAndGet("PRAGMA foreign_keys").getInt(); if (fk_enabled != 1) { MERROR("Failed to enable foreign key constraints; perhaps this sqlite3 is compiled without it?"); throw std::runtime_error{"Foreign key support is required"}; } // FIXME: SQLite / SQLiteCPP may not have encryption available // so this may fail, or worse silently fail and do nothing if (not db_password.empty()) { db.key(std::string{db_password}); } } ~Database() = default; }; } // namespace db