From 081a0414c7941335d474bfadca75ba22645c55d8 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 2 Apr 2025 08:18:09 +0200 Subject: [PATCH] Support for CREATE TABLE in sqlite --- include/sqlgen/Connection.hpp | 5 +- include/sqlgen/internal/collect.hpp | 6 +++ include/sqlgen/internal/collect/vector.hpp | 20 +++++++ include/sqlgen/internal/strings/strings.hpp | 24 +++++++++ include/sqlgen/sqlite.hpp | 7 +++ include/sqlgen/sqlite/Connection.hpp | 29 +++++----- include/sqlgen/sqlite/connect.hpp | 16 ++++++ src/sqlgen.cpp | 1 + src/sqlgen/internal/strings/strings.cpp | 58 ++++++++++++++++++++ src/sqlgen/sqlite/Connection.cpp | 59 ++++++++++++++++++++- tests/CMakeLists.txt | 4 ++ tests/sqlite/CMakeLists.txt | 19 +++++++ tests/sqlite/test_to_create_table.cpp | 24 +++++++++ 13 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 include/sqlgen/internal/collect.hpp create mode 100644 include/sqlgen/internal/collect/vector.hpp create mode 100644 include/sqlgen/internal/strings/strings.hpp create mode 100644 include/sqlgen/sqlite.hpp create mode 100644 include/sqlgen/sqlite/connect.hpp create mode 100644 src/sqlgen/internal/strings/strings.cpp create mode 100644 tests/sqlite/CMakeLists.txt create mode 100644 tests/sqlite/test_to_create_table.cpp diff --git a/include/sqlgen/Connection.hpp b/include/sqlgen/Connection.hpp index d212f51..26f0ced 100644 --- a/include/sqlgen/Connection.hpp +++ b/include/sqlgen/Connection.hpp @@ -22,11 +22,14 @@ struct Connection { /// Executes a statement. Note that in order for the statement to take effect, /// you must call .commit() afterwards. - virtual Result execute(const dynamic::Statement& _stmt) = 0; + virtual Result execute(const std::string& _sql) = 0; /// Reads the results of a SelectFrom statement. virtual Result> read(const dynamic::SelectFrom& _query) = 0; + /// Transpiles a statement to a particular SQL dialect. + virtual std::string to_sql(const dynamic::Statement& _stmt) = 0; + /// Starts the write operation. virtual Result start_write(const dynamic::Insert& _stmt) = 0; diff --git a/include/sqlgen/internal/collect.hpp b/include/sqlgen/internal/collect.hpp new file mode 100644 index 0000000..f9d25c0 --- /dev/null +++ b/include/sqlgen/internal/collect.hpp @@ -0,0 +1,6 @@ +#ifndef SQLGEN_INTERNAL_COLLECT_HPP_ +#define SQLGEN_INTERNAL_COLLECT_HPP_ + +namespace sqlgen::internal::collect {} + +#endif diff --git a/include/sqlgen/internal/collect/vector.hpp b/include/sqlgen/internal/collect/vector.hpp new file mode 100644 index 0000000..5b1f0c5 --- /dev/null +++ b/include/sqlgen/internal/collect/vector.hpp @@ -0,0 +1,20 @@ +#ifndef SQLGEN_INTERNAL_COLLECT_VECTOR_HPP_ +#define SQLGEN_INTERNAL_COLLECT_VECTOR_HPP_ + +#include + +namespace sqlgen::internal::collect { + +template +auto vector(RangeType _r) { + using T = std::ranges::range_value_t; + std::vector res; + for (auto e : _r) { + res.emplace_back(std::move(e)); + } + return res; +} + +} // namespace sqlgen::internal::collect + +#endif diff --git a/include/sqlgen/internal/strings/strings.hpp b/include/sqlgen/internal/strings/strings.hpp new file mode 100644 index 0000000..9d45571 --- /dev/null +++ b/include/sqlgen/internal/strings/strings.hpp @@ -0,0 +1,24 @@ +#ifndef SQLGEN_INTERNAL_STRINGS_STRINGS_HPP_ +#define SQLGEN_INTERNAL_STRINGS_STRINGS_HPP_ + +#include +#include + +namespace sqlgen::internal::strings { + +char to_lower(const char ch); + +char to_upper(const char ch); + +std::string join(const std::string& _delimiter, + const std::vector& _strings); + +std::string replace_all(const std::string& _str, const std::string& _from, + const std::string& _to); + +std::vector split(const std::string& _str, + const std::string& _delimiter); + +} // namespace sqlgen::internal::strings + +#endif diff --git a/include/sqlgen/sqlite.hpp b/include/sqlgen/sqlite.hpp new file mode 100644 index 0000000..eb388b1 --- /dev/null +++ b/include/sqlgen/sqlite.hpp @@ -0,0 +1,7 @@ +#ifndef SQLGEN_SQLITE_HPP_ +#define SQLGEN_SQLITE_HPP_ + +#include "../sqlgen.hpp" +#include "sqlite/connect.hpp" + +#endif diff --git a/include/sqlgen/sqlite/Connection.hpp b/include/sqlgen/sqlite/Connection.hpp index bd2f7f5..d912294 100644 --- a/include/sqlgen/sqlite/Connection.hpp +++ b/include/sqlgen/sqlite/Connection.hpp @@ -1,5 +1,5 @@ -#ifndef SQLGEN_SQLITE3_CONNECTION_HPP_ -#define SQLGEN_SQLITE3_CONNECTION_HPP_ +#ifndef SQLGEN_SQLITE_CONNECTION_HPP_ +#define SQLGEN_SQLITE_CONNECTION_HPP_ #include @@ -11,6 +11,7 @@ #include "../Connection.hpp" #include "../Ref.hpp" #include "../Result.hpp" +#include "../dynamic/Column.hpp" #include "../dynamic/Statement.hpp" namespace sqlgen::sqlite { @@ -30,11 +31,9 @@ class Connection : public sqlgen::Connection { ~Connection(); - Result commit() final { return exec("COMMIT;"); } + Result commit() final { return execute("COMMIT;"); } - Result execute(const dynamic::Statement& _stmt) final { - return exec(to_sql(_stmt)); - } + Result execute(const std::string& _sql) noexcept final; Connection& operator=(const Connection& _other) = delete; @@ -44,6 +43,8 @@ class Connection : public sqlgen::Connection { return error("TODO"); } + std::string to_sql(const dynamic::Statement& _stmt) noexcept final; + Result start_write(const dynamic::Insert& _stmt) final { return error("TODO"); } @@ -56,18 +57,22 @@ class Connection : public sqlgen::Connection { } private: + /// Transforms a dynamic::Column to an SQL string that defines the column in a + /// CREATE TABLE statement. + std::string column_to_sql_definition(const dynamic::Column& _col) noexcept; + /// 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); + /// Expresses the properies as SQL. + std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept; + + /// Expresses the type as SQL. + std::string type_to_sql(const dynamic::Type& _type) noexcept; + private: /// The underlying sqlite3 connection. sqlite3* conn_; diff --git a/include/sqlgen/sqlite/connect.hpp b/include/sqlgen/sqlite/connect.hpp new file mode 100644 index 0000000..eb0a85f --- /dev/null +++ b/include/sqlgen/sqlite/connect.hpp @@ -0,0 +1,16 @@ +#ifndef SQLGEN_SQLITE_CONNECT_HPP_ +#define SQLGEN_SQLITE_CONNECT_HPP_ + +#include + +#include "Connection.hpp" + +namespace sqlgen::sqlite { + +inline auto connect(const std::string& _fname = ":memory:") { + return Connection::make(_fname); +} + +} // namespace sqlgen::sqlite + +#endif diff --git a/src/sqlgen.cpp b/src/sqlgen.cpp index e69de29..fbfbb36 100644 --- a/src/sqlgen.cpp +++ b/src/sqlgen.cpp @@ -0,0 +1 @@ +#include "sqlgen/internal/strings/strings.cpp" diff --git a/src/sqlgen/internal/strings/strings.cpp b/src/sqlgen/internal/strings/strings.cpp new file mode 100644 index 0000000..20890cc --- /dev/null +++ b/src/sqlgen/internal/strings/strings.cpp @@ -0,0 +1,58 @@ +#include "sqlgen/internal/strings/strings.hpp" + +namespace sqlgen::internal::strings { + +char to_lower(const char ch) { + if (ch >= 'A' && ch <= 'Z') { + return ch + ('a' - 'A'); + } else { + return ch; + } +} + +char to_upper(const char ch) { + if (ch >= 'a' && ch <= 'z') { + return ch + ('A' - 'a'); + } else { + return ch; + } +} + +std::string join(const std::string& _delimiter, + const std::vector& _strings) { + if (_strings.size() == 0) { + return ""; + } + auto res = _strings[0]; + for (size_t i = 1; i < _strings.size(); ++i) { + res += _delimiter + _strings[i]; + } + return res; +} + +std::string replace_all(const std::string& _str, const std::string& _from, + const std::string& _to) { + auto str = _str; + + size_t pos = 0; + while ((pos = str.find(_from, pos)) != std::string::npos) { + str.replace(pos, _from.length(), _to); + pos += _to.length(); + } + return str; +} + +std::vector split(const std::string& _str, + const std::string& _delimiter) { + auto str = _str; + size_t pos = 0; + std::vector result; + while ((pos = str.find(_delimiter)) != std::string::npos) { + result.emplace_back(str.substr(0, pos)); + str.erase(0, pos + _delimiter.length()); + } + result.emplace_back(std::move(str)); + return result; +} + +} // namespace sqlgen::internal::strings diff --git a/src/sqlgen/sqlite/Connection.cpp b/src/sqlgen/sqlite/Connection.cpp index ce57bc8..837afba 100644 --- a/src/sqlgen/sqlite/Connection.cpp +++ b/src/sqlgen/sqlite/Connection.cpp @@ -1,7 +1,12 @@ #include "sqlgen/sqlite/Connection.hpp" +#include +#include #include +#include "sqlgen/internal/collect/vector.hpp" +#include "sqlgen/internal/strings/strings.hpp" + namespace sqlgen::sqlite { Connection::~Connection() { @@ -10,8 +15,21 @@ Connection::~Connection() { } } +std::string Connection::column_to_sql_definition( + const dynamic::Column& _col) noexcept { + return "\"" + _col.name + "\"" + " " + type_to_sql(_col.type) + + properties_to_sql( + _col.type.visit([](const auto& _t) { return _t.properties; })); +} + std::string Connection::create_table_to_sql( const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + const auto col_to_sql = [&](const auto& _col) { + return column_to_sql_definition(_col); + }; + std::stringstream stream; stream << "CREATE TABLE "; if (_stmt.table.schema) { @@ -21,9 +39,12 @@ std::string Connection::create_table_to_sql( if (_stmt.if_not_exists) { stream << "IF NOT EXISTS "; } - stream << "("; + stream << "("; + stream << internal::strings::join( + ", ", internal::collect::vector(_stmt.columns | transform(col_to_sql))); stream << ");"; + return stream.str(); } @@ -36,7 +57,7 @@ rfl::Result> Connection::make( } } -Result Connection::exec(const std::string& _sql) noexcept { +Result Connection::execute(const std::string& _sql) noexcept { char* errmsg = nullptr; sqlite3_exec(conn_, _sql.c_str(), nullptr, nullptr, &errmsg); if (errmsg) { @@ -57,6 +78,12 @@ sqlite3* Connection::make_conn(const std::string& _fname) { return conn; } +std::string Connection::properties_to_sql( + const dynamic::types::Properties& _p) noexcept { + return std::string(_p.primary ? " PRIMARY KEY" : "") + + std::string(_p.nullable ? "" : " NOT NULL"); +} + Connection& Connection::operator=(Connection&& _other) { if (this == &_other) { return *this; @@ -78,4 +105,32 @@ std::string Connection::to_sql(const dynamic::Statement& _stmt) noexcept { }); } +std::string Connection::type_to_sql(const dynamic::Type& _type) noexcept { + return _type.visit([](const auto _t) -> std::string { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return "INTEGER"; + } else if constexpr (std::is_same_v || + std::is_same_v) { + return "REAL"; + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + return "TEXT"; + } else { + static_assert(rfl::always_false_v, "Not all cases were covered."); + } + }); +} + } // namespace sqlgen::sqlite diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6ab3ad5..8808bb0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,3 +22,7 @@ target_link_libraries( find_package(GTest) gtest_discover_tests(sqlgen-tests) + +if(SQLGEN_SQLITE3) + add_subdirectory(sqlite) +endif() diff --git a/tests/sqlite/CMakeLists.txt b/tests/sqlite/CMakeLists.txt new file mode 100644 index 0000000..a744712 --- /dev/null +++ b/tests/sqlite/CMakeLists.txt @@ -0,0 +1,19 @@ +project(sqlgen-sqlite-tests) + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp") + +add_executable( + sqlgen-sqlite-tests + ${SOURCES} +) +target_precompile_headers(sqlgen-sqlite-tests PRIVATE [["sqlgen.hpp"]] ) + + +target_link_libraries( + sqlgen-sqlite-tests + PRIVATE + "${SQLGEN_GTEST_LIB}" +) + +find_package(GTest) +gtest_discover_tests(sqlgen-sqlite-tests) diff --git a/tests/sqlite/test_to_create_table.cpp b/tests/sqlite/test_to_create_table.cpp new file mode 100644 index 0000000..2a185a7 --- /dev/null +++ b/tests/sqlite/test_to_create_table.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +#include + +namespace test_to_create_table { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(sqlite, test_to_create_table) { + const auto create_table_stmt = sqlgen::parsing::to_create_table(); + const auto conn = sqlgen::sqlite::connect().value(); + const auto expected = + R"(CREATE TABLE "TestTable" IF NOT EXISTS ("field1" TEXT NOT NULL, "field2" INTEGER NOT NULL, "id" INTEGER PRIMARY KEY NOT NULL, "nullable" TEXT);)"; + + EXPECT_EQ(conn->to_sql(create_table_stmt), expected); +} +} // namespace test_to_create_table