diff --git a/CMakeLists.txt b/CMakeLists.txt index 24ff837..fc2d410 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,33 +7,30 @@ project(podrm LANGUAGES C CXX) add_subdirectory(vendor) find_package(fmt REQUIRED) -find_package(PostgreSQL REQUIRED) -find_package(SQLite3 REQUIRED) - -add_library(podrm STATIC) -target_compile_features(podrm PUBLIC cxx_std_20) -target_sources(podrm PRIVATE lib/sqlite/utils.cpp lib/sqlite/operations.cpp - lib/postgres/utils.cpp lib/postgres/operations.cpp) -target_include_directories(podrm SYSTEM PUBLIC include) -target_link_libraries( - podrm - PUBLIC Boost::pfr - PRIVATE PostgreSQL::PostgreSQL SQLite::SQLite3 fmt::fmt) + +add_library(podrm-reflection INTERFACE) + +target_compile_features(podrm-reflection INTERFACE cxx_std_20) +target_include_directories(podrm-reflection SYSTEM INTERFACE include) +target_link_libraries(podrm-reflection INTERFACE Boost::pfr) + +add_subdirectory(lib/postgres) +add_subdirectory(lib/sqlite) option(PFR_ORM_USE_GSL_SPAN "Use Microsoft.GSL for span implementation instead of std::span" OFF) if(PFR_ORM_USE_GSL_SPAN) - target_compile_definitions(podrm PUBLIC PFR_ORM_USE_GSL_SPAN) + target_compile_definitions(podrm-reflection INTERFACE PFR_ORM_USE_GSL_SPAN) find_package(Microsoft.GSL REQUIRED) - target_link_libraries(podrm PUBLIC Microsoft.GSL::GSL) + target_link_libraries(podrm-reflection INTERFACE Microsoft.GSL::GSL) endif() if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) option(PFR_ORM_ASAN "Build podrm with address sanitizer" OFF) if(PFR_ORM_ASAN) - target_compile_options(podrm PRIVATE -fsanitize=address) - target_link_options(podrm PRIVATE -fsanitize=address) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) endif() include(CTest) diff --git a/include/podrm/postgres/database.hpp b/include/podrm/postgres/database.hpp new file mode 100644 index 0000000..9778277 --- /dev/null +++ b/include/podrm/postgres/database.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace podrm::postgres { + +class Database { +public: + Database(const std::string &connectionStr) + : Database(detail::Connection{connectionStr}) {} + + template void createTable() { + return this->connection.createTable(DatabaseEntityDescription); + } + + template bool exists() { + return this->connection.exists(DatabaseEntityDescription); + } + +private: + detail::Connection connection; + + explicit Database(detail::Connection &&connection) + : connection(std::move(connection)) {} +}; + +} // namespace podrm::postgres diff --git a/include/podrm/postgres/detail/connection.hpp b/include/podrm/postgres/detail/connection.hpp new file mode 100644 index 0000000..2add542 --- /dev/null +++ b/include/podrm/postgres/detail/connection.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include +#include + +struct pg_conn; + +namespace podrm::postgres::detail { + +class Connection { +public: + Connection(const std::string &connectionStr); + ~Connection(); + + void createTable(const EntityDescription &entity); + + bool exists(const EntityDescription &entity); + + Connection(const Connection &) = delete; + Connection(Connection &&) noexcept; + Connection &operator=(const Connection &) = delete; + Connection &operator=(Connection &&) = delete; + + [[nodiscard]] Str escapeIdentifier(std::string_view identifier) const; + +private: + pg_conn *connection; + + Result execute(const std::string &statement); + + Result query(const std::string &statement); +}; + +} // namespace podrm::postgres::detail diff --git a/include/podrm/postgres/detail/operations.hpp b/include/podrm/postgres/detail/operations.hpp deleted file mode 100644 index 55794a1..0000000 --- a/include/podrm/postgres/detail/operations.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include -#include - -namespace podrm::postgres::detail { - -void createTable(Connection &connection, const EntityDescription &entity); - -bool exists(Connection &connection, const EntityDescription &entity); - -} // namespace podrm::postgres::detail diff --git a/include/podrm/postgres/detail/result.hpp b/include/podrm/postgres/detail/result.hpp new file mode 100644 index 0000000..b1bd42c --- /dev/null +++ b/include/podrm/postgres/detail/result.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +struct pg_result; + +namespace podrm::postgres::detail { + +class Result { +public: + Result(pg_result *result) : result(result) {} + ~Result(); + + [[nodiscard]] int status() const; + [[nodiscard]] std::string_view value(int row, int column) const; + + Result(const Result &) = delete; + Result(Result &&) noexcept; + Result &operator=(const Result &) = delete; + Result &operator=(Result &&) = delete; + +private: + pg_result *result; +}; + +} // namespace podrm::postgres::detail diff --git a/include/podrm/postgres/detail/str.hpp b/include/podrm/postgres/detail/str.hpp new file mode 100644 index 0000000..be6e191 --- /dev/null +++ b/include/podrm/postgres/detail/str.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace podrm::postgres::detail { + +class Str { +public: + Str(char *str) : str(str) {} + ~Str(); + + [[nodiscard]] std::string_view view() const { return str; } + operator std::string_view() const { return str; } + + Str(const Str &) = delete; + Str(Str &&) = delete; + Str &operator=(const Str &) = delete; + Str &operator=(Str &&) = delete; + +private: + char *str; +}; + +} // namespace podrm::postgres::detail diff --git a/include/podrm/postgres/operations.hpp b/include/podrm/postgres/operations.hpp deleted file mode 100644 index e01410a..0000000 --- a/include/podrm/postgres/operations.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace podrm::postgres { - -template void createTable(Connection &connection) { - return detail::createTable(connection, DatabaseEntityDescription); -} - -template bool exists(Connection &connection) { - return detail::exists(connection, DatabaseEntityDescription); -} - -} // namespace podrm::postgres diff --git a/include/podrm/postgres/utils.hpp b/include/podrm/postgres/utils.hpp deleted file mode 100644 index 888f5ea..0000000 --- a/include/podrm/postgres/utils.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include -#include -#include - -struct pg_conn; -struct pg_result; - -namespace podrm::postgres { - -class Str { -public: - Str(char *str) : str(str) {} - ~Str(); - - [[nodiscard]] std::string_view view() const { return str; } - operator std::string_view() const { return str; } - - Str(const Str &) = delete; - Str(Str &&) = delete; - Str &operator=(const Str &) = delete; - Str &operator=(Str &&) = delete; - -private: - char *str; -}; - -class Result { -public: - Result(pg_result *result) : result(result) {} - ~Result(); - - [[nodiscard]] int status() const; - [[nodiscard]] std::string_view value(int row, int column) const; - - Result(const Result &) = delete; - Result(Result &&) noexcept; - Result &operator=(const Result &) = delete; - Result &operator=(Result &&) = delete; - -private: - pg_result *result; -}; - -class Connection { -public: - Connection(const std::string &connectionStr); - ~Connection(); - - Connection(const Connection &) = delete; - Connection(Connection &&) noexcept; - Connection &operator=(const Connection &) = delete; - Connection &operator=(Connection &&) = delete; - - [[nodiscard]] Str escapeIdentifier(std::string_view identifier) const; - - Result execute(const std::string &statement); - - Result query(const std::string &statement); - -private: - pg_conn *connection; -}; - -struct ParameterTraits { - struct Parameter { - std::string data; - bool isBinary; // TODO: pass everything as binary - }; - - template static Parameter toParam(const T &value) = delete; -}; - -template <> -inline ParameterTraits::Parameter -ParameterTraits::toParam(const int64_t &value) { - return { - .data = std::to_string(value), - .isBinary = false, - }; -} - -template <> -inline ParameterTraits::Parameter -ParameterTraits::toParam(const std::string &value) { - return { - .data = value, - .isBinary = true, - }; -} - -template -concept AsParameter = requires { ParameterTraits::toParam; }; - -} // namespace podrm::postgres diff --git a/include/podrm/sqlite/database.hpp b/include/podrm/sqlite/database.hpp new file mode 100644 index 0000000..c2a44e3 --- /dev/null +++ b/include/podrm/sqlite/database.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace podrm::sqlite { + +class Database { +public: + //---------------- Constructors ------------------// + + static Database fromRaw(sqlite3 &connection) { + return Database{detail::Connection::fromRaw(connection)}; + } + + static Database inMemory(const char *name) { + return Database{detail::Connection::inMemory(name)}; + } + + static Database inFile(const std::filesystem::path &path) { + return Database{detail::Connection::inFile(path)}; + } + + //---------------- Operations ------------------// + + template void createTable() { + return this->connection.createTable(DatabaseEntityDescription); + } + + template bool exists() { + return this->connection.exists(DatabaseEntityDescription); + } + + template void persist(Entity &entity) { + return this->connection.persist(DatabaseEntityDescription, &entity); + } + + template + std::optional find(const PrimaryKeyType &key) { + Entity result; + if (!this->connection.find(DatabaseEntityDescription, key, + &result)) { + return std::nullopt; + } + + return result; + } + + template + void erase(const PrimaryKeyType &key) { + this->connection.erase(DatabaseEntityDescription, key); + } + + template void update(const Entity &entity) { + this->connection.update(DatabaseEntityDescription, &entity); + } + +private: + detail::Connection connection; + + explicit Database(detail::Connection &&connection) + : connection(std::move(connection)) {} +}; + +} // namespace podrm::sqlite diff --git a/include/podrm/sqlite/detail/connection.hpp b/include/podrm/sqlite/detail/connection.hpp new file mode 100644 index 0000000..917ef4e --- /dev/null +++ b/include/podrm/sqlite/detail/connection.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct sqlite3; + +namespace podrm::sqlite::detail { + +class Connection { +public: + //---------------- Constructors ------------------// + + static Connection fromRaw(sqlite3 &connection); + + static Connection inMemory(const char *name); + + static Connection inFile(const std::filesystem::path &path); + + //---------------- Operations ------------------// + + void createTable(const EntityDescription &entity); + + bool exists(const EntityDescription &entity); + + void persist(const EntityDescription &description, void *entity); + + /// @param[out] result pointer to the result structure, filled if found + bool find(const EntityDescription &description, const AsImage &key, + void *result); + + void erase(EntityDescription description, const AsImage &key); + + void update(EntityDescription description, const void *entity); + +private: + std::unique_ptr connection; + + std::unique_ptr mutex = std::make_unique(); + + explicit Connection(sqlite3 &connection); + + /// @returns number of affected entries + std::uint64_t execute(std::string_view statement, + span args = {}); + + Result query(std::string_view statement, span args = {}); +}; + +} // namespace podrm::sqlite::detail diff --git a/include/podrm/sqlite/detail/entry.hpp b/include/podrm/sqlite/detail/entry.hpp new file mode 100644 index 0000000..d937896 --- /dev/null +++ b/include/podrm/sqlite/detail/entry.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include +#include + +struct sqlite3_stmt; + +namespace podrm::sqlite::detail { + +class Entry { +public: + [[nodiscard]] std::string_view text() const; + + [[nodiscard]] std::int64_t bigint() const; + + [[nodiscard]] double real() const; + + [[nodiscard]] bool boolean() const; + + [[nodiscard]] span bytes() const; + +private: + sqlite3_stmt *statement; + + int column; + + friend class Row; + + explicit Entry(sqlite3_stmt *statement, int column); +}; + +} // namespace podrm::sqlite::detail diff --git a/include/podrm/sqlite/detail/operations.hpp b/include/podrm/sqlite/detail/operations.hpp deleted file mode 100644 index 03f10dc..0000000 --- a/include/podrm/sqlite/detail/operations.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace podrm::sqlite::detail { - -void createTable(Connection &connection, const EntityDescription &entity); - -bool exists(Connection &connection, const EntityDescription &entity); - -void persist(Connection &connection, const EntityDescription &description, - void *entity); - -/// @param[out] result pointer to the result structure, filled if found -bool find(Connection &connection, const EntityDescription &description, - const AsImage &key, void *result); - -void erase(Connection &connection, EntityDescription description, - const AsImage &key); - -void update(Connection &connection, EntityDescription description, - const void *entity); - -} // namespace podrm::sqlite::detail diff --git a/include/podrm/sqlite/detail/result.hpp b/include/podrm/sqlite/detail/result.hpp new file mode 100644 index 0000000..fb2f9c8 --- /dev/null +++ b/include/podrm/sqlite/detail/result.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include + +namespace podrm::sqlite::detail { + +class Result { +public: + [[nodiscard]] std::optional getRow() const { + if (!this->statement.has_value()) { + return std::nullopt; + } + return Row{statement->get(), this->columnCount}; + } + + bool nextRow(); + + [[nodiscard]] int getColumnCount() const { return this->columnCount; } + +private: + using Statement = std::unique_ptr; + + std::optional statement; + + int columnCount = 0; + + friend class Connection; + + explicit Result(Statement statement); +}; + +} // namespace podrm::sqlite::detail diff --git a/include/podrm/sqlite/detail/row.hpp b/include/podrm/sqlite/detail/row.hpp new file mode 100644 index 0000000..c95b769 --- /dev/null +++ b/include/podrm/sqlite/detail/row.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include + +struct sqlite3_stmt; + +namespace podrm::sqlite::detail { + +class Row { +public: + class InvalidRowError : public std::out_of_range { + public: + using std::out_of_range::out_of_range; + }; + + [[nodiscard]] int getColumnCount() const { return this->columnCount; } + + /// @throws InvalidRowError if the column is outside of the range + [[nodiscard]] Entry get(int column) const; + +private: + sqlite3_stmt *statement; + + int columnCount; + + friend class Result; + + explicit Row(sqlite3_stmt *const statement, const int columnCount) + : statement(statement), columnCount(columnCount) {} +}; + +} // namespace podrm::sqlite::detail diff --git a/include/podrm/sqlite/operations.hpp b/include/podrm/sqlite/operations.hpp deleted file mode 100644 index 1d291ef..0000000 --- a/include/podrm/sqlite/operations.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace podrm::sqlite { - -template void createTable(Connection &connection) { - return detail::createTable(connection, DatabaseEntityDescription); -} - -template bool exists(Connection &connection) { - return detail::exists(connection, DatabaseEntityDescription); -} - -template -void persist(Connection &connection, Entity &entity) { - return detail::persist(connection, DatabaseEntityDescription, - &entity); -} - -template -std::optional find(Connection &connection, - const PrimaryKeyType &key) { - Entity result; - if (!detail::find(connection, DatabaseEntityDescription, key, - &result)) { - return std::nullopt; - } - - return result; -} - -template -void erase(Connection &connection, const PrimaryKeyType &key) { - detail::erase(connection, DatabaseEntityDescription, key); -} - -template -void update(Connection &connection, const Entity &entity) { - detail::update(connection, DatabaseEntityDescription, &entity); -} - -} // namespace podrm::sqlite diff --git a/include/podrm/sqlite/utils.hpp b/include/podrm/sqlite/utils.hpp deleted file mode 100644 index d98e842..0000000 --- a/include/podrm/sqlite/utils.hpp +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -struct sqlite3; -struct sqlite3_stmt; -struct sqlite3_value; - -namespace podrm::sqlite { - -class Entry { -public: - [[nodiscard]] std::string_view text() const; - - [[nodiscard]] std::int64_t bigint() const; - - [[nodiscard]] double real() const; - - [[nodiscard]] bool boolean() const; - - [[nodiscard]] span bytes() const; - -private: - sqlite3_stmt *statement; - - int column; - - friend class Row; - - explicit Entry(sqlite3_stmt *statement, int column); -}; - -class Row { -public: - class InvalidRowError : public std::out_of_range { - public: - using std::out_of_range::out_of_range; - }; - - [[nodiscard]] int getColumnCount() const { return this->columnCount; } - - /// @throws InvalidRowError if the column is outside of the range - [[nodiscard]] Entry get(int column) const; - -private: - sqlite3_stmt *statement; - - int columnCount; - - friend class Result; - - explicit Row(sqlite3_stmt *const statement, const int columnCount) - : statement(statement), columnCount(columnCount) {} -}; - -class Result { -public: - [[nodiscard]] std::optional getRow() const { - if (!this->statement.has_value()) { - return std::nullopt; - } - return Row{statement->get(), this->columnCount}; - } - - bool nextRow(); - - [[nodiscard]] int getColumnCount() const { return this->columnCount; } - -private: - using Statement = std::unique_ptr; - - std::optional statement; - - int columnCount = 0; - - friend class Connection; - - explicit Result(Statement statement); -}; - -class Connection { -public: - static Connection fromRaw(sqlite3 &connection); - - static Connection inMemory(const char *name); - - static Connection inFile(const std::filesystem::path &path); - - /// @returns number of affected entries - std::uint64_t execute(std::string_view statement, - span args = {}); - - Result query(std::string_view statement, span args = {}); - -private: - std::unique_ptr connection; - - std::mutex mutex; - - explicit Connection(sqlite3 &connection); -}; - -} // namespace podrm::sqlite diff --git a/lib/postgres/CMakeLists.txt b/lib/postgres/CMakeLists.txt new file mode 100644 index 0000000..4e07920 --- /dev/null +++ b/lib/postgres/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(PostgreSQL REQUIRED) + +add_library(podrm-postgres STATIC) +target_sources(podrm-postgres PRIVATE connection.cpp result.cpp str.cpp) +target_link_libraries( + podrm-postgres + PUBLIC podrm-reflection + PRIVATE PostgreSQL::PostgreSQL fmt::fmt) diff --git a/lib/postgres/operations.cpp b/lib/postgres/connection.cpp similarity index 59% rename from lib/postgres/operations.cpp rename to lib/postgres/connection.cpp index d01efcc..ed3fb7f 100644 --- a/lib/postgres/operations.cpp +++ b/lib/postgres/connection.cpp @@ -2,7 +2,9 @@ #include "formatters.hpp" // IWYU pragma: keep #include -#include +#include +#include +#include #include #include @@ -15,6 +17,7 @@ #include #include +#include namespace podrm::postgres::detail { @@ -80,26 +83,62 @@ void createTableFields(const FieldDescription &description, } // namespace -void createTable(Connection &connection, const EntityDescription &entity) { - const Str escapedTableName = connection.escapeIdentifier(entity.name); - connection.execute(fmt::format("DROP TABLE IF EXISTS {}", escapedTableName)); +Connection::Connection(const std::string &connectionStr) + : connection(PQconnectdb(connectionStr.c_str())) { + if (PQstatus(this->connection) != CONNECTION_OK) { + throw std::runtime_error{fmt::format("Failed to connect to db: {}", + PQerrorMessage(this->connection))}; + } +} + +Connection::~Connection() { PQfinish(this->connection); } + +Str Connection::escapeIdentifier(const std::string_view identifier) const { + return Str{PQescapeIdentifier(this->connection, identifier.data(), + identifier.size())}; +} + +Result Connection::execute(const std::string &statement) { + Result result{PQexec(this->connection, statement.c_str())}; + if (result.status() != PGRES_COMMAND_OK) { + throw std::runtime_error{ + fmt::format("Error when executing a statement: {}", + PQerrorMessage(this->connection)), + }; + } + return result; +} + +Result Connection::query(const std::string &statement) { + Result result{PQexec(this->connection, statement.c_str())}; + if (result.status() != PGRES_TUPLES_OK) { + throw std::runtime_error{ + fmt::format("Error when executing a query: {}", + PQerrorMessage(this->connection)), + }; + } + return result; +} + +void Connection::createTable(const EntityDescription &entity) { + const Str escapedTableName = this->escapeIdentifier(entity.name); + this->execute(fmt::format("DROP TABLE IF EXISTS {}", escapedTableName)); fmt::memory_buffer buf; fmt::appender appender{buf}; fmt::format_to(appender, "CREATE TABLE {} (", escapedTableName); bool first = true; for (std::size_t i = 0; i < entity.fields.size(); ++i) { - createTableFields(entity.fields[i], entity.primaryKey == i, connection, - appender, {}, first); + createTableFields(entity.fields[i], entity.primaryKey == i, *this, appender, + {}, first); } fmt::format_to(appender, ")"); - connection.execute(fmt::to_string(buf)); + this->execute(fmt::to_string(buf)); } -bool exists(Connection &connection, const EntityDescription &entity) { - const Result result = - connection.query(fmt::format("SELECT EXISTS(SELECT 1 FROM {})", - connection.escapeIdentifier(entity.name))); +bool Connection::exists(const EntityDescription &entity) { + const Result result = this->query(fmt::format( + "SELECT EXISTS(SELECT 1 FROM {})", this->escapeIdentifier(entity.name))); return result.value(0, 0)[0] == 't'; } diff --git a/lib/postgres/formatters.hpp b/lib/postgres/formatters.hpp index cadad3f..eeb934c 100644 --- a/lib/postgres/formatters.hpp +++ b/lib/postgres/formatters.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include @@ -8,10 +8,11 @@ #include template <> -struct fmt::formatter : formatter { +struct fmt::formatter + : formatter { public: template - constexpr auto format(const podrm::postgres::Str &str, + constexpr auto format(const podrm::postgres::detail::Str &str, FormatContext &ctx) const -> decltype(ctx.out()) { return formatter::format(str.view(), ctx); } diff --git a/lib/postgres/result.cpp b/lib/postgres/result.cpp new file mode 100644 index 0000000..7d9d6a5 --- /dev/null +++ b/lib/postgres/result.cpp @@ -0,0 +1,27 @@ +#include + +#include + +#include + +namespace podrm::postgres::detail { + +Result::Result(Result &&other) noexcept : result(other.result) { + other.result = nullptr; +} + +Result::~Result() { + if (this->result != nullptr) { + PQclear(this->result); + } +} + +[[nodiscard]] int Result::status() const { + return PQresultStatus(this->result); +} + +std::string_view Result::value(const int row, const int column) const { + return PQgetvalue(this->result, row, column); +} + +} // namespace podrm::postgres::detail diff --git a/lib/postgres/str.cpp b/lib/postgres/str.cpp new file mode 100644 index 0000000..c3c87d1 --- /dev/null +++ b/lib/postgres/str.cpp @@ -0,0 +1,9 @@ +#include + +#include + +namespace podrm::postgres::detail { + +Str::~Str() { PQfreemem(this->str); } + +} // namespace podrm::postgres::detail diff --git a/lib/postgres/utils.cpp b/lib/postgres/utils.cpp deleted file mode 100644 index 346f977..0000000 --- a/lib/postgres/utils.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include - -#include -#include -#include - -#include -#include - -namespace podrm::postgres { - -Str::~Str() { PQfreemem(this->str); } - -Result::Result(Result &&other) noexcept : result(other.result) { - other.result = nullptr; -} - -Result::~Result() { - if (this->result != nullptr) { - PQclear(this->result); - } -} - -[[nodiscard]] int Result::status() const { - return PQresultStatus(this->result); -} - -std::string_view Result::value(const int row, const int column) const { - return PQgetvalue(this->result, row, column); -} - -Connection::Connection(const std::string &connectionStr) - : connection(PQconnectdb(connectionStr.c_str())) { - if (PQstatus(this->connection) != CONNECTION_OK) { - throw std::runtime_error{fmt::format("Failed to connect to db: {}", - PQerrorMessage(this->connection))}; - } -} - -Connection::~Connection() { PQfinish(this->connection); } - -Str Connection::escapeIdentifier(const std::string_view identifier) const { - return Str{PQescapeIdentifier(this->connection, identifier.data(), - identifier.size())}; -} - -Result Connection::execute(const std::string &statement) { - Result result{PQexec(this->connection, statement.c_str())}; - if (result.status() != PGRES_COMMAND_OK) { - throw std::runtime_error{ - fmt::format("Error when executing a statement: {}", - PQerrorMessage(this->connection)), - }; - } - return result; -} - -Result Connection::query(const std::string &statement) { - Result result{PQexec(this->connection, statement.c_str())}; - if (result.status() != PGRES_TUPLES_OK) { - throw std::runtime_error{ - fmt::format("Error when executing a query: {}", - PQerrorMessage(this->connection)), - }; - } - return result; -} - -} // namespace podrm::postgres diff --git a/lib/sqlite/CMakeLists.txt b/lib/sqlite/CMakeLists.txt new file mode 100644 index 0000000..a5b64d0 --- /dev/null +++ b/lib/sqlite/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(SQLite3 REQUIRED) + +add_library(podrm-sqlite STATIC) +target_sources(podrm-sqlite PRIVATE connection.cpp entry.cpp result.cpp row.cpp) +target_link_libraries( + podrm-sqlite + PUBLIC podrm-reflection + PRIVATE SQLite::SQLite3 fmt::fmt) diff --git a/lib/sqlite/operations.cpp b/lib/sqlite/connection.cpp similarity index 67% rename from lib/sqlite/operations.cpp rename to lib/sqlite/connection.cpp index 18828cc..dc82749 100644 --- a/lib/sqlite/operations.cpp +++ b/lib/sqlite/connection.cpp @@ -3,12 +3,17 @@ #include #include #include -#include -#include +#include +#include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -19,11 +24,61 @@ #include #include +#include namespace podrm::sqlite::detail { namespace { +using Statement = + std::unique_ptr; + +Statement createStatement(sqlite3 &connection, + const std::string_view statement) { + sqlite3_stmt *stmt = nullptr; + const int result = + sqlite3_prepare_v3(&connection, statement.data(), + static_cast(statement.size()), 0, &stmt, nullptr); + if (result != SQLITE_OK) { + throw std::runtime_error{sqlite3_errmsg(&connection)}; + } + + return Statement{stmt}; +} + +void bindArg(const Statement &statement, const int pos, const AsImage &value) { + const auto bindInt = [&statement, pos](const std::int64_t value) { + sqlite3_bind_int64(statement.get(), pos + 1, value); + }; + const auto bindUInt = [&statement, pos](const std::uint64_t value) { + if (value > std::numeric_limits::max()) { + throw std::invalid_argument{"Unsigned integer too big"}; + } + sqlite3_bind_int64(statement.get(), pos + 1, + static_cast(value)); + }; + const auto bindBlob = [&statement, pos](const span blob) { + sqlite3_bind_blob64(statement.get(), pos, blob.data(), blob.size(), + SQLITE_STATIC); + }; + const auto bindDouble = [&statement, pos](const double value) { + sqlite3_bind_double(statement.get(), pos + 1, value); + }; + const auto bindText = [&statement, pos](const std::string_view text) { + sqlite3_bind_text64(statement.get(), pos + 1, text.data(), text.size(), + SQLITE_STATIC, SQLITE_UTF8); + }; + const auto bindBool = [&statement, pos](const bool value) { + sqlite3_bind_int(statement.get(), pos + 1, value ? 1 : 0); + }; + + std::visit(podrm::detail::MultiLambda{bindBlob, bindDouble, bindText, bindInt, + bindUInt, bindBool}, + value); +} + std::string_view toString(const ImageType type) { switch (type) { case ImageType::Bool: @@ -224,8 +279,72 @@ void init(const FieldDescription description, const Row row, int ¤tColumn, } // namespace -void createTable(Connection &connection, const EntityDescription &entity) { - connection.execute(fmt::format("DROP TABLE IF EXISTS '{}'", entity.name)); +Connection::Connection(sqlite3 &connection) + : connection(&connection, &sqlite3_close_v2) { + this->execute("PRAGMA foreign_keys = ON"); +} + +Connection Connection::fromRaw(sqlite3 &connection) { + return Connection{connection}; +} + +Connection Connection::inMemory(const char *const name) { + sqlite3 *connection = nullptr; + const int result = sqlite3_open_v2( + name, &connection, + SQLITE_OPEN_MEMORY | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); + if (result != SQLITE_OK) { + throw std::runtime_error{sqlite3_errstr(result)}; + } + + return Connection{*connection}; +} + +Connection Connection::inFile(const std::filesystem::path &path) { + sqlite3 *connection = nullptr; + const int result = + sqlite3_open_v2(path.string().c_str(), &connection, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); + if (result != SQLITE_OK) { + throw std::runtime_error{sqlite3_errstr(result)}; + } + + return Connection{*connection}; +} + +std::uint64_t Connection::execute(const std::string_view statement, + const span args) { + const std::unique_lock lock{*this->mutex}; + + const Statement stmt = createStatement(*this->connection, statement); + + for (int i = 0; i < args.size(); ++i) { + bindArg(stmt, i, args[i]); + } + + const int executeResult = sqlite3_step(stmt.get()); + if (executeResult != SQLITE_DONE) { + throw std::runtime_error{sqlite3_errmsg(this->connection.get())}; + } + + return sqlite3_changes64(this->connection.get()); +} + +Result Connection::query(const std::string_view statement, + const span args) { + const std::unique_lock lock{*this->mutex}; + + Statement stmt = createStatement(*this->connection, statement); + + for (int i = 0; i < args.size(); ++i) { + bindArg(stmt, i, args[i]); + } + + return Result{{stmt.release(), &sqlite3_finalize}}; +} + +void Connection::createTable(const EntityDescription &entity) { + this->execute(fmt::format("DROP TABLE IF EXISTS '{}'", entity.name)); fmt::memory_buffer buf; fmt::appender appender{buf}; @@ -241,18 +360,17 @@ void createTable(Connection &connection, const EntityDescription &entity) { } fmt::format_to(appender, ")"); - connection.execute(fmt::to_string(buf)); + this->execute(fmt::to_string(buf)); } -bool exists(Connection &connection, const EntityDescription &entity) { - const Result result = connection.query( +bool Connection::exists(const EntityDescription &entity) { + const Result result = this->query( fmt::format("SELECT EXISTS(SELECT 1 FROM '{}')", entity.name)); // NOLINTNEXTLINE(bugprone-unchecked-optional-access): fixed query return result.getRow().value().get(0).boolean(); } -void persist(Connection &connection, const EntityDescription &description, - void *entity) { +void Connection::persist(const EntityDescription &description, void *entity) { fmt::memory_buffer buf; fmt::appender appender{buf}; @@ -274,17 +392,17 @@ void persist(Connection &connection, const EntityDescription &description, } fmt::format_to(appender, ")"); - connection.execute(fmt::to_string(buf), values); + this->execute(fmt::to_string(buf), values); } -bool find(Connection &connection, const EntityDescription &description, - const AsImage &key, void *result) { +bool Connection::find(const EntityDescription &description, const AsImage &key, + void *result) { const std::string queryStr = fmt::format("SELECT * FROM '{}' WHERE {} = ?", description.name, description.fields[description.primaryKey].name); const Result query = - connection.query(queryStr, podrm::span{&key, 1}); + this->query(queryStr, podrm::span{&key, 1}); std::optional row = query.getRow(); if (!row.has_value()) { return false; @@ -299,21 +417,21 @@ bool find(Connection &connection, const EntityDescription &description, return true; } -void erase(Connection &connection, const EntityDescription description, - const AsImage &key) { +void Connection::erase(const EntityDescription description, + const AsImage &key) { const std::string queryStr = fmt::format("DELETE FROM '{}' WHERE {} = ?", description.name, description.fields[description.primaryKey].name); const std::uint64_t changes = - connection.execute(queryStr, podrm::span{&key, 1}); + this->execute(queryStr, podrm::span{&key, 1}); if (changes == 0) { throw std::runtime_error("Entity with the given key is not found"); } } -void update(Connection &connection, const EntityDescription description, - const void *entity) { +void Connection::update(const EntityDescription description, + const void *entity) { fmt::memory_buffer buf; fmt::appender appender{buf}; @@ -340,7 +458,7 @@ void update(Connection &connection, const EntityDescription description, } values.emplace_back(std::move(key[0])); - const std::uint64_t changes = connection.execute(fmt::to_string(buf), values); + const std::uint64_t changes = this->execute(fmt::to_string(buf), values); if (changes == 0) { throw std::runtime_error("Entity with the given key is not found"); } diff --git a/lib/sqlite/entry.cpp b/lib/sqlite/entry.cpp new file mode 100644 index 0000000..3592337 --- /dev/null +++ b/lib/sqlite/entry.cpp @@ -0,0 +1,47 @@ +#include +#include + +#include +#include +#include +#include + +#include + +namespace podrm::sqlite::detail { + +std::string_view Entry::text() const { + return { + // Required and safe + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): + reinterpret_cast( + sqlite3_column_text(this->statement, this->column)), + static_cast( + sqlite3_column_bytes(this->statement, this->column)), + }; +} + +std::int64_t Entry::bigint() const { + + return sqlite3_column_int64(this->statement, this->column); +} + +double Entry::real() const { + return sqlite3_column_double(this->statement, this->column); +} + +bool Entry::boolean() const { return this->bigint() != 0; } + +span Entry::bytes() const { + return { + static_cast( + sqlite3_column_blob(this->statement, this->column)), + static_cast( + sqlite3_column_bytes(this->statement, this->column)), + }; +} + +Entry::Entry(sqlite3_stmt *const statement, const int column) + : statement(statement), column(column) {} + +} // namespace podrm::sqlite::detail diff --git a/lib/sqlite/result.cpp b/lib/sqlite/result.cpp new file mode 100644 index 0000000..ffffa03 --- /dev/null +++ b/lib/sqlite/result.cpp @@ -0,0 +1,35 @@ +#include + +#include +#include +#include + +#include + +namespace podrm::sqlite::detail { + +Result::Result(Result::Statement statement) : statement(std::move(statement)) { + this->nextRow(); +} + +bool Result::nextRow() { + assert(this->statement.has_value()); + + const int result = sqlite3_step(this->statement->get()); + if (result == SQLITE_DONE) { + this->statement.reset(); + return false; + } + + if (result != SQLITE_ROW) { + throw std::runtime_error{ + sqlite3_errmsg(sqlite3_db_handle(this->statement->get())), + }; + } + + this->columnCount = sqlite3_data_count(this->statement->get()); + + return true; +} + +} // namespace podrm::sqlite::detail diff --git a/lib/sqlite/row.cpp b/lib/sqlite/row.cpp new file mode 100644 index 0000000..57f9384 --- /dev/null +++ b/lib/sqlite/row.cpp @@ -0,0 +1,18 @@ +#include +#include + +#include + +namespace podrm::sqlite::detail { + +Entry Row::get(const int column) const { + if (column < 0 || column > this->columnCount) { + throw InvalidRowError{ + fmt::format("Column {} is out of range, result contains {} columns", + column, this->columnCount)}; + } + + return Entry{this->statement, column}; +} + +} // namespace podrm::sqlite::detail diff --git a/lib/sqlite/utils.cpp b/lib/sqlite/utils.cpp deleted file mode 100644 index a37dbcf..0000000 --- a/lib/sqlite/utils.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include "../detail/multilambda.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace podrm::sqlite { - -namespace { - -using Statement = - std::unique_ptr; - -Statement createStatement(sqlite3 &connection, - const std::string_view statement) { - sqlite3_stmt *stmt = nullptr; - const int result = - sqlite3_prepare_v3(&connection, statement.data(), - static_cast(statement.size()), 0, &stmt, nullptr); - if (result != SQLITE_OK) { - throw std::runtime_error{sqlite3_errmsg(&connection)}; - } - - return Statement{stmt}; -} - -void bindArg(const Statement &statement, const int pos, const AsImage &value) { - const auto bindInt = [&statement, pos](const std::int64_t value) { - sqlite3_bind_int64(statement.get(), pos + 1, value); - }; - const auto bindUInt = [&statement, pos](const std::uint64_t value) { - if (value > std::numeric_limits::max()) { - throw std::invalid_argument{"Unsigned integer too big"}; - } - sqlite3_bind_int64(statement.get(), pos + 1, - static_cast(value)); - }; - const auto bindBlob = [&statement, pos](const span blob) { - sqlite3_bind_blob64(statement.get(), pos, blob.data(), blob.size(), - SQLITE_STATIC); - }; - const auto bindDouble = [&statement, pos](const double value) { - sqlite3_bind_double(statement.get(), pos + 1, value); - }; - const auto bindText = [&statement, pos](const std::string_view text) { - sqlite3_bind_text64(statement.get(), pos + 1, text.data(), text.size(), - SQLITE_STATIC, SQLITE_UTF8); - }; - const auto bindBool = [&statement, pos](const bool value) { - sqlite3_bind_int(statement.get(), pos + 1, value ? 1 : 0); - }; - - std::visit(detail::MultiLambda{bindBlob, bindDouble, bindText, bindInt, - bindUInt, bindBool}, - value); -} - -} // namespace - -std::string_view Entry::text() const { - return { - // Required and safe - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): - reinterpret_cast( - sqlite3_column_text(this->statement, this->column)), - static_cast( - sqlite3_column_bytes(this->statement, this->column)), - }; -} - -std::int64_t Entry::bigint() const { - - return sqlite3_column_int64(this->statement, this->column); -} - -double Entry::real() const { - return sqlite3_column_double(this->statement, this->column); -} - -bool Entry::boolean() const { return this->bigint() != 0; } - -span Entry::bytes() const { - return { - static_cast( - sqlite3_column_blob(this->statement, this->column)), - static_cast( - sqlite3_column_bytes(this->statement, this->column)), - }; -} - -Entry::Entry(sqlite3_stmt *const statement, const int column) - : statement(statement), column(column) {} - -Entry Row::get(const int column) const { - if (column < 0 || column > this->columnCount) { - throw InvalidRowError{ - fmt::format("Column {} is out of range, result contains {} columns", - column, this->columnCount)}; - } - - return Entry{this->statement, column}; -} - -Result::Result(Result::Statement statement) : statement(std::move(statement)) { - this->nextRow(); -} - -bool Result::nextRow() { - assert(this->statement.has_value()); - - const int result = sqlite3_step(this->statement->get()); - if (result == SQLITE_DONE) { - this->statement.reset(); - return false; - } - - if (result != SQLITE_ROW) { - throw std::runtime_error{ - sqlite3_errmsg(sqlite3_db_handle(this->statement->get())), - }; - } - - this->columnCount = sqlite3_data_count(this->statement->get()); - - return true; -} - -Connection::Connection(sqlite3 &connection) - : connection(&connection, &sqlite3_close_v2) { - this->execute("PRAGMA foreign_keys = ON"); -} - -Connection Connection::fromRaw(sqlite3 &connection) { - return Connection{connection}; -} - -Connection Connection::inMemory(const char *const name) { - sqlite3 *connection = nullptr; - const int result = sqlite3_open_v2( - name, &connection, - SQLITE_OPEN_MEMORY | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); - if (result != SQLITE_OK) { - throw std::runtime_error{sqlite3_errstr(result)}; - } - - return Connection{*connection}; -} - -Connection Connection::inFile(const std::filesystem::path &path) { - sqlite3 *connection = nullptr; - const int result = - sqlite3_open_v2(path.string().c_str(), &connection, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); - if (result != SQLITE_OK) { - throw std::runtime_error{sqlite3_errstr(result)}; - } - - return Connection{*connection}; -} - -std::uint64_t Connection::execute(const std::string_view statement, - const span args) { - const std::unique_lock lock{this->mutex}; - - const Statement stmt = createStatement(*this->connection, statement); - - for (int i = 0; i < args.size(); ++i) { - bindArg(stmt, i, args[i]); - } - - const int executeResult = sqlite3_step(stmt.get()); - if (executeResult != SQLITE_DONE) { - throw std::runtime_error{sqlite3_errmsg(this->connection.get())}; - } - - return sqlite3_changes64(this->connection.get()); -} - -Result Connection::query(const std::string_view statement, - const span args) { - const std::unique_lock lock{this->mutex}; - - Statement stmt = createStatement(*this->connection, statement); - - for (int i = 0; i < args.size(); ++i) { - bindArg(stmt, i, args[i]); - } - - return Result{{stmt.release(), &sqlite3_finalize}}; -} - -} // namespace podrm::sqlite diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 84a089b..0df39da 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,7 +20,8 @@ find_package(Catch2 3 REQUIRED) add_executable(${PROJECT_NAME} test.cpp nested.cpp relation.cpp self_relation.cpp simple.cpp) -target_link_libraries(${PROJECT_NAME} podrm fmt::fmt Catch2::Catch2WithMain) +target_link_libraries(${PROJECT_NAME} podrm-sqlite fmt::fmt + Catch2::Catch2WithMain) include(CTest) include(Catch) diff --git a/test/test.cpp b/test/test.cpp index 4999c30..58c1b81 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -4,8 +4,7 @@ #include #include #include -#include -#include +#include #include #include @@ -61,13 +60,13 @@ constexpr auto podrm::EntityRegistration = static_assert(podrm::DatabaseEntity); TEST_CASE("SQLite works", "[sqlite]") { - orm::Connection conn = orm::Connection::inMemory("test"); + orm::Database db = orm::Database::inMemory("test"); - REQUIRE_NOTHROW(orm::createTable
(conn)); - REQUIRE_NOTHROW(orm::createTable(conn)); + REQUIRE_NOTHROW(db.createTable
()); + REQUIRE_NOTHROW(db.createTable()); - REQUIRE_FALSE(orm::exists
(conn)); - REQUIRE_FALSE(orm::exists(conn)); + REQUIRE_FALSE(db.exists
()); + REQUIRE_FALSE(db.exists()); SECTION("Foreign key constraints are enforced") { Person person{ @@ -76,7 +75,7 @@ TEST_CASE("SQLite works", "[sqlite]") { .address{.key = 42}, }; - CHECK_THROWS(orm::persist(conn, person)); + CHECK_THROWS(db.persist(person)); } Address address{ @@ -84,7 +83,7 @@ TEST_CASE("SQLite works", "[sqlite]") { .postalCode = "abc", }; - REQUIRE_NOTHROW(orm::persist(conn, address)); + REQUIRE_NOTHROW(db.persist(address)); Person person{ .id = 0, @@ -92,15 +91,15 @@ TEST_CASE("SQLite works", "[sqlite]") { .address{.key = address.id}, }; - REQUIRE_NOTHROW(orm::persist(conn, person)); + REQUIRE_NOTHROW(db.persist(person)); SECTION("find on non-existent id returns nullopt") { - const std::optional person = orm::find(conn, 42); + const std::optional person = db.find(42); CHECK_FALSE(person.has_value()); } SECTION("erase on non-existent id throws") { - CHECK_THROWS(orm::erase(conn, 42)); + CHECK_THROWS(db.erase(42)); } SECTION("update on non-existent id throws") { @@ -110,34 +109,31 @@ TEST_CASE("SQLite works", "[sqlite]") { .address{.key = address.id}, }; - CHECK_THROWS(orm::update(conn, newPerson)); + CHECK_THROWS(db.update(newPerson)); } SECTION("erase of referenced entity throws") { - REQUIRE_THROWS(orm::erase
(conn, address.id)); + REQUIRE_THROWS(db.erase
(address.id)); } SECTION("find on existing id returns existing value") { - const std::optional personFound = - orm::find(conn, person.id); + const std::optional personFound = db.find(person.id); REQUIRE(personFound.has_value()); CHECK(person == *personFound); } SECTION("erase on existing id erases existing value") { - REQUIRE_NOTHROW(orm::erase(conn, person.id)); + REQUIRE_NOTHROW(db.erase(person.id)); - const std::optional personFound = - orm::find(conn, person.id); + const std::optional personFound = db.find(person.id); CHECK_FALSE(personFound.has_value()); } SECTION("update on existing id updates existing value") { person.name = "Anne"; - REQUIRE_NOTHROW(orm::update(conn, person)); + REQUIRE_NOTHROW(db.update(person)); - const std::optional personFound = - orm::find(conn, person.id); + const std::optional personFound = db.find(person.id); REQUIRE(personFound.has_value()); CHECK(personFound->name == "Anne"); }