diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c88178..1eed59d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,13 +12,8 @@ if (NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 20) endif() - set(SQLGEN_USE_VCPKG_DEFAULT ON) -if(SQLGEN_BUILD_BENCHMARKS) - # TODO -endif() - option(SQLGEN_USE_VCPKG "Use VCPKG to download and build dependencies" ${SQLGEN_USE_VCPKG_DEFAULT}) if (SQLGEN_USE_VCPKG) @@ -49,6 +44,14 @@ target_include_directories( $ $) +if (SQLGEN_SQLITE3) + list(APPEND SQLGEN_SOURCES src/sqlgen_sqlite.cpp) + if (NOT TARGET unofficial-sqlite3) + find_package(unofficial-sqlite3 CONFIG REQUIRED) + endif() + target_link_libraries(sqlgen PUBLIC unofficial::sqlite3::sqlite3) +endif() + find_package(reflectcpp CONFIG REQUIRED) target_link_libraries(sqlgen PUBLIC reflectcpp::reflectcpp) diff --git a/README.md b/README.md index 44a96e8..d593da5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ sqlgen is closely integrated with our sister project [reflect-cpp](https://githu ## Simple example -Here is how you create a simple sqlite3 database +Here is how you create a simple sqlite database and insert some data: ```cpp @@ -24,7 +24,7 @@ const auto people = std::vector({ .last_name = "Simpson", .age = 45}}); -const auto conn = sqlgen::sqlite3::connect("example.db"); +const auto conn = sqlgen::sqlite::connect("example.db"); // Will automatically create a table called 'People' // with the columns 'first_name', 'last_name' and 'age', @@ -43,7 +43,7 @@ and print the results as a JSON: #include // reflect-cpp #include -const auto conn = sqlgen::sqlite3::connect("example.db"); +const auto conn = sqlgen::sqlite::connect("example.db"); const sqlgen::Result> result = sqlgen::read>(conn); diff --git a/include/sqlgen/Connection.hpp b/include/sqlgen/Connection.hpp index 2d946d1..d212f51 100644 --- a/include/sqlgen/Connection.hpp +++ b/include/sqlgen/Connection.hpp @@ -8,9 +8,9 @@ #include "Iterator.hpp" #include "Ref.hpp" #include "Result.hpp" -#include "dynamic/CreateTable.hpp" #include "dynamic/Insert.hpp" #include "dynamic/SelectFrom.hpp" +#include "dynamic/Statement.hpp" namespace sqlgen { @@ -22,8 +22,7 @@ struct Connection { /// Executes a statement. Note that in order for the statement to take effect, /// you must call .commit() afterwards. - /// TODO: Abstract away the different statements using rfl::TaggedUnion. - virtual Result execute(const dynamic::CreateTable& _stmt) = 0; + virtual Result execute(const dynamic::Statement& _stmt) = 0; /// Reads the results of a SelectFrom statement. virtual Result> read(const dynamic::SelectFrom& _query) = 0; diff --git a/include/sqlgen/Result.hpp b/include/sqlgen/Result.hpp index a75f521..cad200f 100644 --- a/include/sqlgen/Result.hpp +++ b/include/sqlgen/Result.hpp @@ -2,6 +2,7 @@ #define SQLGEN_RESULT_HPP_ #include +#include namespace sqlgen { @@ -12,6 +13,8 @@ using Nothing = rfl::Nothing; template using Result = rfl::Result; +inline auto error(const std::string& _msg) { return rfl::error(_msg); } + }; // namespace sqlgen #endif diff --git a/include/sqlgen/dynamic/Column.hpp b/include/sqlgen/dynamic/Column.hpp index 32b7183..e7bf5ad 100644 --- a/include/sqlgen/dynamic/Column.hpp +++ b/include/sqlgen/dynamic/Column.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_DYNAMIC_COLUMN_HPP_ #define SQLGEN_DYNAMIC_COLUMN_HPP_ +#include #include #include @@ -9,9 +10,9 @@ namespace sqlgen::dynamic { struct Column { - std::string alias; - std::string name; - Type type; + std::optional alias; + std::string name; + Type type; }; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/dynamic/Statement.hpp b/include/sqlgen/dynamic/Statement.hpp new file mode 100644 index 0000000..80bd6af --- /dev/null +++ b/include/sqlgen/dynamic/Statement.hpp @@ -0,0 +1,14 @@ +#ifndef SQLGEN_DYNAMIC_STATEMENT_HPP_ +#define SQLGEN_DYNAMIC_STATEMENT_HPP_ + +#include + +#include "CreateTable.hpp" + +namespace sqlgen::dynamic { + +using Statement = rfl::TaggedUnion<"stmt", CreateTable>; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/dynamic/Table.hpp b/include/sqlgen/dynamic/Table.hpp index 6d8d13b..b5ddafb 100644 --- a/include/sqlgen/dynamic/Table.hpp +++ b/include/sqlgen/dynamic/Table.hpp @@ -9,9 +9,9 @@ namespace sqlgen::dynamic { struct Table { - std::string alias; + std::optional alias; std::string name; - std::string schema; + std::optional schema; }; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/parsing/get_schema.hpp b/include/sqlgen/parsing/get_schema.hpp index 35436d2..cb8bb4f 100644 --- a/include/sqlgen/parsing/get_schema.hpp +++ b/include/sqlgen/parsing/get_schema.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_PARSING_GET_SCHEMA_HPP_ #define SQLGEN_PARSING_GET_SCHEMA_HPP_ +#include #include #include @@ -9,13 +10,13 @@ namespace sqlgen::parsing { template -std::string get_schema() noexcept { +std::optional get_schema() noexcept { using Type = std::remove_cvref_t; if constexpr (has_schema) { using LiteralType = typename Type::schema; return LiteralType().str(); } else { - return ""; + return std::nullopt; } } diff --git a/include/sqlgen/sqlite/Connection.hpp b/include/sqlgen/sqlite/Connection.hpp new file mode 100644 index 0000000..bd2f7f5 --- /dev/null +++ b/include/sqlgen/sqlite/Connection.hpp @@ -0,0 +1,78 @@ +#ifndef SQLGEN_SQLITE3_CONNECTION_HPP_ +#define SQLGEN_SQLITE3_CONNECTION_HPP_ + +#include + +#include +#include +#include +#include + +#include "../Connection.hpp" +#include "../Ref.hpp" +#include "../Result.hpp" +#include "../dynamic/Statement.hpp" + +namespace sqlgen::sqlite { + +class Connection : public sqlgen::Connection { + public: + Connection(const std::string& _fname) : conn_(make_conn(_fname)) {} + + Connection(const Connection& _other) = delete; + + Connection(Connection&& _other) : conn_(_other.conn_) { + _other.conn_ = nullptr; + } + + static rfl::Result> make( + const std::string& _fname) noexcept; + + ~Connection(); + + Result commit() final { return exec("COMMIT;"); } + + Result execute(const dynamic::Statement& _stmt) final { + return exec(to_sql(_stmt)); + } + + Connection& operator=(const Connection& _other) = delete; + + Connection& operator=(Connection&& _other); + + Result> read(const dynamic::SelectFrom& _query) final { + return error("TODO"); + } + + Result start_write(const dynamic::Insert& _stmt) final { + return error("TODO"); + } + + Result end_write() final { return error("TODO"); } + + Result write( + const std::vector>>& _data) final { + return error("TODO"); + } + + private: + /// Transforms a CreateTable Statement to an SQL string. + std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept; + + /// Wrapper around sqlite3_exec. + Result exec(const std::string& _sql) noexcept; + + /// Transforms a Statement to an SQL string. + std::string to_sql(const dynamic::Statement& _stmt) noexcept; + + /// Generates the underlying connection. + static sqlite3* make_conn(const std::string& _fname); + + private: + /// The underlying sqlite3 connection. + sqlite3* conn_; +}; + +} // namespace sqlgen::sqlite + +#endif diff --git a/src/sqlgen/sqlite/Connection.cpp b/src/sqlgen/sqlite/Connection.cpp new file mode 100644 index 0000000..ce57bc8 --- /dev/null +++ b/src/sqlgen/sqlite/Connection.cpp @@ -0,0 +1,81 @@ +#include "sqlgen/sqlite/Connection.hpp" + +#include + +namespace sqlgen::sqlite { + +Connection::~Connection() { + if (conn_) { + sqlite3_close(conn_); + } +} + +std::string Connection::create_table_to_sql( + const dynamic::CreateTable& _stmt) noexcept { + std::stringstream stream; + stream << "CREATE TABLE "; + if (_stmt.table.schema) { + stream << "\"" << *_stmt.table.schema << "\"."; + } + stream << "\"" << _stmt.table.name << "\" "; + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + stream << "("; + + stream << ");"; + return stream.str(); +} + +rfl::Result> Connection::make( + const std::string& _fname) noexcept { + try { + return Ref(Ref::make(_fname)); + } catch (std::exception& e) { + return error(e.what()); + } +} + +Result Connection::exec(const std::string& _sql) noexcept { + char* errmsg = nullptr; + sqlite3_exec(conn_, _sql.c_str(), nullptr, nullptr, &errmsg); + if (errmsg) { + const auto err = error(errmsg); + sqlite3_free(errmsg); + return err; + } + return Nothing{}; +} + +sqlite3* Connection::make_conn(const std::string& _fname) { + sqlite3* conn = nullptr; + const auto err = sqlite3_open(_fname.c_str(), &conn); + if (err) { + throw std::runtime_error("Can't open database: " + + std::string(sqlite3_errmsg(conn))); + } + return conn; +} + +Connection& Connection::operator=(Connection&& _other) { + if (this == &_other) { + return *this; + } + sqlite3_close(conn_); + conn_ = _other.conn_; + _other.conn_ = nullptr; + return *this; +} + +std::string Connection::to_sql(const dynamic::Statement& _stmt) noexcept { + return _stmt.visit([&](const auto& _s) -> std::string { + using S = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return create_table_to_sql(_s); + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + }); +} + +} // namespace sqlgen::sqlite diff --git a/src/sqlgen_sqlite.cpp b/src/sqlgen_sqlite.cpp new file mode 100644 index 0000000..ec44eec --- /dev/null +++ b/src/sqlgen_sqlite.cpp @@ -0,0 +1 @@ +#include "sqlgen/sqlite/Connection.cpp" diff --git a/tests/test_to_create_table.cpp b/tests/test_to_create_table.cpp index 07ae55e..ecebe37 100644 --- a/tests/test_to_create_table.cpp +++ b/tests/test_to_create_table.cpp @@ -17,7 +17,7 @@ TEST(general, test_to_create_table) { const auto create_table_stmt = sqlgen::parsing::to_create_table(); const std::string expected = - R"({"table":{"alias":"","name":"TestTable","schema":""},"columns":[{"alias":"","name":"field1","type":{"type":"Text","properties":{"primary":false,"nullable":false}}},{"alias":"","name":"field2","type":{"type":"Int32","properties":{"primary":false,"nullable":false}}},{"alias":"","name":"id","type":{"type":"UInt32","properties":{"primary":true,"nullable":false}}},{"alias":"","name":"nullable","type":{"type":"Text","properties":{"primary":false,"nullable":true}}}],"if_not_exists":true})"; + R"({"table":{"name":"TestTable"},"columns":[{"name":"field1","type":{"type":"Text","properties":{"primary":false,"nullable":false}}},{"name":"field2","type":{"type":"Int32","properties":{"primary":false,"nullable":false}}},{"name":"id","type":{"type":"UInt32","properties":{"primary":true,"nullable":false}}},{"name":"nullable","type":{"type":"Text","properties":{"primary":false,"nullable":true}}}],"if_not_exists":true})"; const auto json_str = rfl::json::write(create_table_stmt);