module; #include export module routemon:sqlite3; import std; import :util; namespace routemon::sqlite3 { class mutex_guard { explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} { ::sqlite3_mutex_enter(mut_); } friend auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())); public: mutex_guard(mutex_guard const&) = delete; ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } private: ::sqlite3_mutex* mut_; }; auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())) { return f(mutex_guard{mut}); } auto do_guarded(::sqlite3* dbc, std::invocable auto f) -> decltype(f(std::declval())) { return do_guarded(::sqlite3_db_mutex(dbc), f); } class error : public std::exception { int code_; std::string message_; public: explicit error(mutex_guard const&, int code, ::sqlite3* dbc) : code_{code}, message_{::sqlite3_errmsg(dbc)} { } explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} [[nodiscard]] auto what() const noexcept -> char const* override { return message_.c_str(); } [[nodiscard]] auto code() const noexcept -> int { return code_; } }; template concept C> concept optional_of = requires { typename T::value_type; requires std::same_as>; requires C; }; template concept scannable_prim = std::same_as || std::same_as || std::same_as; template concept scannable = scannable_prim || optional_of; class statement { ::sqlite3_stmt* stmt_; public: explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} statement(statement const&) = delete; statement(statement&& s) noexcept { stmt_ = s.stmt_; s.stmt_ = nullptr; } ~statement() { ::sqlite3_finalize(stmt_); } auto get() -> ::sqlite3_stmt* { return stmt_; } }; class row_reader { statement stmt_; explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} friend class connection; void scan(int col, std::string& s) { if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) throw std::invalid_argument{"invalid type for scan"}; unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); if (!size.has_value()) throw std::logic_error{"unexpected negative amount of bytes in column"}; s = std::string{reinterpret_cast(chs), *size}; } void scan(int col, double& v) { if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) throw std::invalid_argument{"invalid type for scan"}; v = ::sqlite3_column_double(stmt_.get(), col); } void scan(int col, std::int64_t& v) { if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) throw std::invalid_argument{"invalid type for scan"}; v = ::sqlite3_column_int64(stmt_.get(), col); } void scan(int col, optional_of auto& v) { if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) { v.reset(); } else { typename std::remove_cvref_t::value_type tmp; scan(col, tmp); v = std::move(tmp); } } public: auto next() -> bool { ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); return do_guarded( dbc, [&](auto const& guard) -> bool { auto const s = ::sqlite3_step(stmt_.get()); if (s == SQLITE_ROW) return true; if (s == SQLITE_DONE) return false; throw error{guard, s, dbc}; }); } auto scan(scannable auto&... args) -> void { auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); if (!ncols.has_value()) throw std::logic_error{"got unexpected negative amount of columns"}; if (sizeof...(args) > *ncols) throw std::invalid_argument{ "more scanning arguments provided than columns in result set" }; auto col = 0; (..., scan(col++, args)); } auto scan_single(scannable auto&... args) -> void { if (!next()) throw std::logic_error{"no row in result set"}; scan(args...); if (next()) { throw std::logic_error{"more than one row in result set"}; } } }; class binder { statement& stmt_; explicit binder(statement& stmt) : stmt_{stmt} {} friend class connection; public: auto text(std::string const& param_name, std::string_view str) -> void { int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); if (i == 0) throw std::invalid_argument{std::format( "bind: no parameter with name {} found", param_name)}; auto str_size = util::int_from_size(str.size()); if (!str_size.has_value()) throw std::invalid_argument{"bind: provided text is too long"}; if (auto s = ::sqlite3_bind_text( stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) { throw error{s}; } } static auto noop(binder&) -> void {} }; export class connection { ::sqlite3* dbc_; ::sqlite3_mutex* mut_; explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} { } friend auto open(std::string const& filename) -> connection; public: connection(connection const&) = delete; connection(connection&& c) noexcept { dbc_ = c.dbc_; mut_ = c.mut_; c.dbc_ = nullptr; c.mut_ = nullptr; } [[nodiscard]] auto query( std::string const& sql, std::function const& bf = binder::noop) -> row_reader { ::sqlite3_stmt* pstmt = nullptr; char const* sql_tail = nullptr; auto sql_size = util::int_from_size(sql.size()); if (!sql_size.has_value() || *sql_size >= std::numeric_limits::max() - 1) throw std::invalid_argument{"provided input text too large"}; do_guarded( mut_, [&](auto const& guard) -> void { if (auto s = ::sqlite3_prepare_v2( dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) { if (pstmt != nullptr) { // Use contract_assert when having a compiler with // contracts available ::sqlite3_finalize(pstmt); throw std::logic_error{ "expected stmt to be null after failed preparation" }; } throw error{guard, s, dbc_}; } }); if (!pstmt) throw std::invalid_argument{"provided input text contains no SQL"}; auto stmt = statement{pstmt}; if (sql_tail && std::strlen(sql_tail) > 0) throw std::invalid_argument{ "provided input text contains more than one SQL statement" }; auto b = binder{stmt}; bf(b); return row_reader{std::move(stmt)}; } auto exec( std::string const& sql, std::function const& bf = binder::noop) -> void { auto reader = query(sql, bf); while (reader.next()) ; } ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } }; export auto open(std::string const& filename) -> connection { ::sqlite3* dbc = nullptr; auto s = ::sqlite3_open_v2( filename.c_str(), &dbc, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE, nullptr); if (s != SQLITE_OK) { if (dbc) { do_guarded( dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); } else { throw error{s}; } } return connection{dbc}; } } // namespace routemon::sqlite3