From 9afa1867d72ad1af33a729df5b6d83c1283fcbe3 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 18 May 2025 18:12:55 +0200 Subject: [PATCH] Added create_index --- include/sqlgen.hpp | 1 + include/sqlgen/create_index.hpp | 76 +++++++++++++++++++ include/sqlgen/create_table.hpp | 4 +- include/sqlgen/dynamic/CreateIndex.hpp | 26 +++++++ include/sqlgen/dynamic/CreateTable.hpp | 1 - include/sqlgen/dynamic/Statement.hpp | 5 +- include/sqlgen/transpilation/columns_t.hpp | 34 +++++++++ .../sqlgen/transpilation/to_create_index.hpp | 37 +++++++++ include/sqlgen/transpilation/to_sql.hpp | 14 ++++ src/sqlgen/postgres/to_sql.cpp | 46 ++++++++++- src/sqlgen/sqlite/to_sql.cpp | 47 +++++++++++- tests/postgres/test_create_index.cpp | 44 +++++++++++ tests/postgres/test_create_index_dry.cpp | 27 +++++++ tests/postgres/test_create_table.cpp | 41 ++++++++++ tests/postgres/test_create_table_dry.cpp | 4 +- tests/sqlite/test_create_index.cpp | 33 ++++++++ 16 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 include/sqlgen/create_index.hpp create mode 100644 include/sqlgen/dynamic/CreateIndex.hpp create mode 100644 include/sqlgen/transpilation/columns_t.hpp create mode 100644 include/sqlgen/transpilation/to_create_index.hpp create mode 100644 tests/postgres/test_create_index.cpp create mode 100644 tests/postgres/test_create_index_dry.cpp create mode 100644 tests/postgres/test_create_table.cpp create mode 100644 tests/sqlite/test_create_index.cpp diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index 437f747..fa2f148 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -17,6 +17,7 @@ #include "sqlgen/begin_transaction.hpp" #include "sqlgen/col.hpp" #include "sqlgen/commit.hpp" +#include "sqlgen/create_index.hpp" #include "sqlgen/create_table.hpp" #include "sqlgen/delete_from.hpp" #include "sqlgen/drop.hpp" diff --git a/include/sqlgen/create_index.hpp b/include/sqlgen/create_index.hpp new file mode 100644 index 0000000..df11670 --- /dev/null +++ b/include/sqlgen/create_index.hpp @@ -0,0 +1,76 @@ +#ifndef SQLGEN_CREATEINDEX_HPP_ +#define SQLGEN_CREATEINDEX_HPP_ + +#include + +#include "Ref.hpp" +#include "Result.hpp" +#include "transpilation/columns_t.hpp" +#include "transpilation/to_create_index.hpp" + +namespace sqlgen { + +template +Result> create_index_impl(const Ref& _conn, + const std::string& _name, + const bool _unique, + const bool _if_not_exists, + const WhereType& _where) { + const auto query = + transpilation::to_create_index( + _name, _unique, _if_not_exists, _where); + return _conn->execute(_conn->to_sql(query)).transform([&](const auto&) { + return _conn; + }); +} + +template +Result> create_index_impl(const Result>& _res, + const std::string& _name, + const bool _unique, + const bool _if_not_exists, + const WhereType& _where) { + return _res.and_then([&](const auto& _conn) { + return create_index_impl( + _conn, _name, _unique, _if_not_exists, _where); + }); +} + +template +struct CreateIndex { + Result> operator()(const auto& _conn) const noexcept { + try { + return create_index_impl, + WhereType>(_conn, _name.str(), unique_, + if_not_exists_, where_); + } catch (std::exception& e) { + return error(e.what()); + } + } + + bool unique_ = false; + bool if_not_exists_ = false; + + WhereType where_; +}; + +template +auto create_index(const ColTypes&...) { + return CreateIndex<_name, ValueType, Nothing, ColTypes...>{ + .unique_ = false, .if_not_exists_ = false}; +} + +template +auto create_unique_index(const ColTypes&...) { + return CreateIndex<_name, ValueType, Nothing, ColTypes...>{ + .unique_ = true, .if_not_exists_ = false}; +} + +}; // namespace sqlgen + +#endif + diff --git a/include/sqlgen/create_table.hpp b/include/sqlgen/create_table.hpp index 223e8f0..950f9ef 100644 --- a/include/sqlgen/create_table.hpp +++ b/include/sqlgen/create_table.hpp @@ -37,8 +37,8 @@ struct CreateTable { bool if_not_exists_; }; -template -const auto create_table = CreateTable{}; +template +const auto create_table = CreateTable{}; }; // namespace sqlgen diff --git a/include/sqlgen/dynamic/CreateIndex.hpp b/include/sqlgen/dynamic/CreateIndex.hpp new file mode 100644 index 0000000..c479feb --- /dev/null +++ b/include/sqlgen/dynamic/CreateIndex.hpp @@ -0,0 +1,26 @@ +#ifndef SQLGEN_DYNAMIC_CREATEINDEX_HPP_ +#define SQLGEN_DYNAMIC_CREATEINDEX_HPP_ + +#include +#include + +#include "Column.hpp" +#include "Condition.hpp" +#include "Table.hpp" + +namespace sqlgen::dynamic { + +struct CreateIndex { + std::string name; + + Table table; + std::vector columns; + + bool unique = false; + bool if_not_exists = true; + std::optional where = std::nullopt; +}; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/dynamic/CreateTable.hpp b/include/sqlgen/dynamic/CreateTable.hpp index b5de2ce..4592df2 100644 --- a/include/sqlgen/dynamic/CreateTable.hpp +++ b/include/sqlgen/dynamic/CreateTable.hpp @@ -13,7 +13,6 @@ struct CreateTable { Table table; std::vector columns; bool if_not_exists = true; - // TODO: Constraints }; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/dynamic/Statement.hpp b/include/sqlgen/dynamic/Statement.hpp index 290fa62..396b3a7 100644 --- a/include/sqlgen/dynamic/Statement.hpp +++ b/include/sqlgen/dynamic/Statement.hpp @@ -3,6 +3,7 @@ #include +#include "CreateIndex.hpp" #include "CreateTable.hpp" #include "DeleteFrom.hpp" #include "Drop.hpp" @@ -12,8 +13,8 @@ namespace sqlgen::dynamic { -using Statement = rfl::TaggedUnion<"stmt", CreateTable, DeleteFrom, Drop, - Insert, SelectFrom, Update>; +using Statement = rfl::TaggedUnion<"stmt", CreateIndex, CreateTable, DeleteFrom, + Drop, Insert, SelectFrom, Update>; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/transpilation/columns_t.hpp b/include/sqlgen/transpilation/columns_t.hpp new file mode 100644 index 0000000..95097bb --- /dev/null +++ b/include/sqlgen/transpilation/columns_t.hpp @@ -0,0 +1,34 @@ +#ifndef SQLGEN_TRANSPILATION_COLUMNST_HPP_ +#define SQLGEN_TRANSPILATION_COLUMNST_HPP_ + +#include +#include +#include + +#include "../col.hpp" +#include "Desc.hpp" +#include "all_columns_exist.hpp" + +namespace sqlgen::transpilation { + +template +struct Columns { + rfl::Tuple<_ColTypes...> values; + + static std::vector to_vec() { + return std::vector({_ColTypes().name()...}); + } +}; + +template +auto make_columns() { + static_assert(all_columns_exist(), "All columns must exist."); + return Columns{}; +} + +template +using columns_t = std::invoke_result_t)>; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_create_index.hpp b/include/sqlgen/transpilation/to_create_index.hpp new file mode 100644 index 0000000..ad7801e --- /dev/null +++ b/include/sqlgen/transpilation/to_create_index.hpp @@ -0,0 +1,37 @@ +#ifndef SQLGEN_TRANSPILATION_TO_CREATE_INDEX_HPP_ +#define SQLGEN_TRANSPILATION_TO_CREATE_INDEX_HPP_ + +#include +#include +#include +#include +#include + +#include "../dynamic/CreateIndex.hpp" +#include "../dynamic/Table.hpp" +#include "get_schema.hpp" +#include "get_tablename.hpp" +#include "to_condition.hpp" + +namespace sqlgen::transpilation { + +template + requires std::is_class_v> && + std::is_aggregate_v> +dynamic::CreateIndex to_create_index(const std::string& _name, + const bool _unique, + const bool _if_not_exists, + const WhereType& _where) { + return dynamic::CreateIndex{ + .name = _name, + .table = + dynamic::Table{.name = get_tablename(), .schema = get_schema()}, + .columns = ColumnsType::to_vec(), + .unique = _unique, + .if_not_exists = _if_not_exists, + .where = to_condition>(_where)}; +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index f9d7cdd..03b8413 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -4,12 +4,15 @@ #include #include "../Insert.hpp" +#include "../create_index.hpp" #include "../create_table.hpp" #include "../delete_from.hpp" #include "../drop.hpp" #include "../dynamic/Statement.hpp" #include "../read.hpp" #include "../update.hpp" +#include "columns_t.hpp" +#include "to_create_index.hpp" #include "to_create_table.hpp" #include "to_delete_from.hpp" #include "to_drop.hpp" @@ -23,6 +26,17 @@ namespace sqlgen::transpilation { template struct ToSQL; +template +struct ToSQL> { + dynamic::Statement operator()(const auto& _create_index) const { + return transpilation::to_create_index< + ValueType, columns_t, WhereType>( + _name.str(), _create_index.unique_, _create_index.if_not_exists_, + _create_index.where_); + } +}; + template struct ToSQL> { dynamic::Statement operator()(const auto&) const { diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index cbd20b7..b089ef4 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -22,6 +22,8 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept; std::string column_to_sql_definition(const dynamic::Column& _col) noexcept; +std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept; + std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept; std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept; @@ -130,6 +132,45 @@ std::string column_to_sql_definition(const dynamic::Column& _col) noexcept { _col.type.visit([](const auto& _t) { return _t.properties; })); } +std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept { + using namespace std::ranges::views; + + std::stringstream stream; + + if (_stmt.unique) { + stream << "CREATE UNIQUE INDEX "; + } else { + stream << "CREATE INDEX "; + } + + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + + stream << "\"" << _stmt.name << "\" "; + + stream << "ON "; + + if (_stmt.table.schema) { + stream << "\"" << *_stmt.table.schema << "\"."; + } + stream << "\"" << _stmt.table.name << "\""; + + stream << "("; + stream << internal::strings::join( + ", ", + internal::collect::vector(_stmt.columns | transform(wrap_in_quotes))); + stream << ")"; + + if (_stmt.where) { + stream << " WHERE " << condition_to_sql(*_stmt.where); + } + + stream << ";"; + + return stream.str(); +} + std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept { using namespace std::ranges::views; @@ -271,7 +312,10 @@ std::string to_sql_impl(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) { + if constexpr (std::is_same_v) { + return create_index_to_sql(_s); + + } else if constexpr (std::is_same_v) { return create_table_to_sql(_s); } else if constexpr (std::is_same_v) { diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index aaae05d..bf54770 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -18,6 +18,8 @@ std::string condition_to_sql(const dynamic::Condition& _cond) noexcept; template std::string condition_to_sql_impl(const ConditionType& _condition) noexcept; +std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept; + std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept; std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept; @@ -112,6 +114,46 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { return stream.str(); } +std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept { + using namespace std::ranges::views; + + const auto in_quotes = [](const std::string& _str) -> std::string { + return "\"" + _str + "\""; + }; + + std::stringstream stream; + + if (_stmt.unique) { + stream << "CREATE UNIQUE INDEX "; + } else { + stream << "CREATE INDEX "; + } + + if (_stmt.if_not_exists) { + stream << "IF NOT EXISTS "; + } + + if (_stmt.table.schema) { + stream << "\"" << *_stmt.table.schema << "\"."; + } + stream << "\"" << _stmt.name << "\" "; + + stream << "ON " << "\"" << _stmt.table.name << "\""; + + stream << "("; + stream << internal::strings::join( + ", ", internal::collect::vector(_stmt.columns | transform(in_quotes))); + stream << ")"; + + if (_stmt.where) { + stream << " WHERE " << condition_to_sql(*_stmt.where); + } + + stream << ";"; + + return stream.str(); +} + std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept { using namespace std::ranges::views; @@ -259,7 +301,10 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { std::string to_sql_impl(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) { + if constexpr (std::is_same_v) { + return create_index_to_sql(_s); + + } else if constexpr (std::is_same_v) { return create_table_to_sql(_s); } else if constexpr (std::is_same_v) { diff --git a/tests/postgres/test_create_index.cpp b/tests/postgres/test_create_index.cpp new file mode 100644 index 0000000..a2f1fae --- /dev/null +++ b/tests/postgres/test_create_index.cpp @@ -0,0 +1,44 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_create_index { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_create_index) { + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto people = sqlgen::postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(create_table | if_not_exists) + .and_then(create_index<"test_table_ix", Person>( + "first_name"_c, "last_name"_c) | + if_not_exists) + .and_then(sqlgen::read>) + .value(); + + const std::string expected = R"([])"; + + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_create_index + +#endif diff --git a/tests/postgres/test_create_index_dry.cpp b/tests/postgres/test_create_index_dry.cpp new file mode 100644 index 0000000..b6660d1 --- /dev/null +++ b/tests/postgres/test_create_index_dry.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include + +namespace test_create_index_dry { + +struct TestTable { + std::string field1; + int32_t field2; + sqlgen::PrimaryKey id; + std::optional nullable; +}; + +TEST(postgres, test_create_index_dry) { + using namespace sqlgen; + + const auto query = + create_index<"test_table_ix", TestTable>("field1"_c, "field2"_c) | + if_not_exists; + + const auto expected = + R"(CREATE INDEX IF NOT EXISTS "test_table_ix" ON "TestTable"("field1", "field2");)"; + + EXPECT_EQ(sqlgen::postgres::to_sql(query), expected); +} +} // namespace test_create_index_dry diff --git a/tests/postgres/test_create_table.cpp b/tests/postgres/test_create_table.cpp new file mode 100644 index 0000000..0c58f03 --- /dev/null +++ b/tests/postgres/test_create_table.cpp @@ -0,0 +1,41 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_create_table { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_create_table) { + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto people = sqlgen::postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(create_table | if_not_exists) + .and_then(sqlgen::read>) + .value(); + + const std::string expected = R"([])"; + + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_create_table + +#endif diff --git a/tests/postgres/test_create_table_dry.cpp b/tests/postgres/test_create_table_dry.cpp index b2705cc..c733c77 100644 --- a/tests/postgres/test_create_table_dry.cpp +++ b/tests/postgres/test_create_table_dry.cpp @@ -13,7 +13,9 @@ struct TestTable { }; TEST(postgres, test_create_table_dry) { - const auto query = sqlgen::CreateTable{}; + using namespace sqlgen; + + const auto query = create_table | if_not_exists; const auto expected = R"(CREATE TABLE IF NOT EXISTS "TestTable" ("field1" TEXT NOT NULL, "field2" INTEGER NOT NULL, "id" INTEGER NOT NULL, "nullable" TEXT, PRIMARY KEY ("id"));)"; diff --git a/tests/sqlite/test_create_index.cpp b/tests/sqlite/test_create_index.cpp new file mode 100644 index 0000000..057e7f5 --- /dev/null +++ b/tests/sqlite/test_create_index.cpp @@ -0,0 +1,33 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_create_index { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_create_index) { + using namespace sqlgen; + + const auto people = sqlgen::sqlite::connect() + .and_then(create_table | if_not_exists) + .and_then(create_index<"person_ix", Person>( + "first_name"_c, "last_name"_c) | + if_not_exists) + .and_then(sqlgen::read>); + + const std::string expected = R"([])"; + + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_create_index