diff --git a/.gitmodules b/.gitmodules index 53e4058..a0a57f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "vcpkg"] path = vcpkg - url = git@github.com:microsoft/vcpkg.git + url = https://github.com/microsoft/vcpkg.git diff --git a/docs/README.md b/docs/README.md index d4fad81..2cddcb1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [Type Conversion Operations](type_conversion_operations.md) - How to convert between types safely in queries (e.g., cast int to double). - [Null Handling Operations](null_handling_operations.md) - How to handle nullable values and propagate nullability correctly (e.g., with coalesce and nullability rules). - [Timestamp and Date/Time Functions](timestamp_operations.md) - How to work with timestamps, dates, and times (e.g., extract parts, perform arithmetic, convert formats). +- [Enums](enum.md) - How to work with enums sqlgen ## Data Types and Validation @@ -61,4 +62,4 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [PostgreSQL](postgres.md) - How to interact with PostgreSQL and compatible databases (Redshift, Aurora, Greenplum, CockroachDB, ...) - [SQLite](sqlite.md) - How to interact with SQLite3 -For installation instructions, quick start guide, and usage examples, please refer to the [main README](../README.md). +For installation instructions, quick start guide, and usage examples, please refer to the [main README](../README.md). \ No newline at end of file diff --git a/docs/enum.md b/docs/enum.md new file mode 100644 index 0000000..47c2f18 --- /dev/null +++ b/docs/enum.md @@ -0,0 +1,37 @@ +# `Enums` + +Enums can directly be used in sqlgen structs. They are mapped to native enum types in PostgreSQL and MySQL. In sqlite, which does not support native enum types, they are stored as `TEXT` by default. + +## Usage + +### Basic Definition + +```cpp +enum class Color : int { RED = 1, GREEN = 2, BLUE = 3 }; +struct Flower { + sqlgen::PrimaryKey name; + Color color; +} + +const auto red_rose = Flower{ + .name = "Rose", + .color = Color::RED, +}; +``` + +This generates the following SQL schema: +```sql +CREATE TYPE IF NOT EXISTS "Color" AS ENUM ('RED', 'GREEN', 'BLUE'); +CREATE TABLE IF NOT EXISTS "Flower"( + "name" TEXT NOT NULL, + "color" Color NOT NULL, + PRIMARY_KEY("name") +); +``` + +## Notes +- Due to naming restrictions in PostgreSQL, the namespace operator `::` is replaced with `__`. Thus, an enum defined as `namespace1::some_struct::MyEnum` will be created in the database as `namespace1__some_struct__MyEnum`. This mangled name can at most be 63 characters long. +- Enums are specific types in PostgreSQL. They are only created once and shared between tables. +- In MySQL, enums are stored as native types but they are not shared between tables. +- In sqlite, enums are stored as `TEXT` by default. If you need to use integers, you can specialize the `Parser` struct as explained [here](dynamic.md). +- You can use `rfl::enum_to_string` and `rfl::string_to_enum` to convert between enum values and their string representations. \ No newline at end of file diff --git a/include/sqlgen/dynamic/Type.hpp b/include/sqlgen/dynamic/Type.hpp index 7180993..c70c091 100644 --- a/include/sqlgen/dynamic/Type.hpp +++ b/include/sqlgen/dynamic/Type.hpp @@ -14,7 +14,7 @@ using Type = types::Int32, types::Int64, types::JSON, types::UInt8, types::UInt16, types::UInt32, types::UInt64, types::Text, types::Date, types::Timestamp, types::TimestampWithTZ, - types::VarChar>; + types::VarChar, types::Enum>; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/dynamic/types.hpp b/include/sqlgen/dynamic/types.hpp index ca7aba7..ec9133f 100644 --- a/include/sqlgen/dynamic/types.hpp +++ b/include/sqlgen/dynamic/types.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace sqlgen::dynamic::types { @@ -78,6 +79,12 @@ struct UInt64 { Properties properties; }; +struct Enum { + std::string name; + std::vector values; + Properties properties; +}; + struct Text { Properties properties; }; diff --git a/include/sqlgen/parsing/Parser_default.hpp b/include/sqlgen/parsing/Parser_default.hpp index 88fbfd8..fccb678 100644 --- a/include/sqlgen/parsing/Parser_default.hpp +++ b/include/sqlgen/parsing/Parser_default.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_PARSING_PARSER_DEFAULT_HPP_ #define SQLGEN_PARSING_PARSER_DEFAULT_HPP_ +#include #include #include #include @@ -33,8 +34,14 @@ struct Parser { return static_cast(std::stod(*_str)); } else if constexpr (std::is_integral_v) { return static_cast(std::stoll(*_str)); - } else if (std::is_same_v) { + } else if constexpr (std::is_same_v) { return std::stoi(*_str) != 0; + } else if constexpr (std::is_enum_v) { + if (auto res = rfl::string_to_enum(*_str)) { + return Type{*res}; + } else { + return error(res.error()); + } } else { static_assert(rfl::always_false_v, "Unsupported type"); } @@ -49,6 +56,8 @@ struct Parser { if constexpr (transpilation::has_reflection_method) { return Parser>::write( _t.reflection()); + } else if constexpr (std::is_enum_v) { + return rfl::enum_to_string(_t); } else { return std::to_string(_t); } @@ -108,6 +117,22 @@ struct Parser { "Unsupported floating point value."); } + } else if constexpr (std::is_enum_v) { + constexpr auto values = rfl::get_enumerator_array(); + std::array enum_names{}; + for (std::size_t i = 0; i < std::size(values); ++i) { + enum_names[i] = values[i].first; + } + constexpr auto org_name = rfl::internal::get_type_name_str_view(); + static_assert(org_name.size() < 64, + "Enum type exceeds type level. If it's defined in a nested " + "namespace, consider moving it up to a higher level."); + // Transform '::' to '__' to avoid postgres limitations + std::string trf_name(org_name); + std::ranges::replace(trf_name, ':', '_'); + return sqlgen::dynamic::types::Enum{ + std::move(trf_name), + std::vector(enum_names.begin(), enum_names.end())}; } else { static_assert(rfl::always_false_v, "Unsupported type."); } diff --git a/include/sqlgen/transpilation/to_value.hpp b/include/sqlgen/transpilation/to_value.hpp index b0f9dd4..45372f9 100644 --- a/include/sqlgen/transpilation/to_value.hpp +++ b/include/sqlgen/transpilation/to_value.hpp @@ -25,6 +25,9 @@ dynamic::Value to_value(const T& _t) { } else if constexpr (has_reflection_method) { return to_value(_t.reflection()); + } else if constexpr (std::is_enum_v) { + return dynamic::Value{dynamic::String{.val = rfl::enum_to_string(_t)}}; + } else { static_assert(rfl::always_false_v, "Unsupported type"); } diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index 8236ae5..6a390e6 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -82,6 +82,10 @@ inline std::string wrap_in_quotes(const std::string& _name) noexcept { return "`" + _name + "`"; } +inline std::string wrap_in_single_quotes(const std::string& _name) noexcept { + return "'" + _name + "'"; +} + // ---------------------------------------------------------------------------- std::string aggregation_to_sql( @@ -154,6 +158,8 @@ std::string cast_type_to_sql(const dynamic::Type& _type) noexcept { } else if constexpr (std::is_same_v) { return "CHAR"; + } else if constexpr (std::is_same_v) { + return "ENUM"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } @@ -872,6 +878,14 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept { std::is_same_v) { return "BIGINT"; + } else if constexpr (std::is_same_v) { + return "ENUM(" + + internal::strings::join( + ", ", internal::collect::vector(_t.values | + std::ranges::views::transform( + wrap_in_single_quotes))) + + ")"; + } else if constexpr (std::is_same_v || std::is_same_v) { return "DECIMAL"; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 95cab03..5595d93 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -42,6 +42,9 @@ std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept; std::vector get_primary_keys( const dynamic::CreateTable& _stmt) noexcept; +std::vector>> get_enum_types( + const dynamic::CreateTable& _stmt) noexcept; + std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept; std::string join_to_sql(const dynamic::Join& _stmt) noexcept; @@ -66,10 +69,26 @@ std::string write_to_sql(const dynamic::Write& _stmt) noexcept; inline std::string get_name(const dynamic::Column& _col) { return _col.name; } +inline std::pair> get_enum_mapping( + const dynamic::Column& _col) { + return _col.type.visit( + [&](const auto& _t) -> std::pair> { + using T = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return {type_to_sql(_t), _t.values}; + } + return {}; + }); +} + inline std::string wrap_in_quotes(const std::string& _name) noexcept { return "\"" + _name + "\""; } +inline std::string wrap_in_single_quotes(const std::string& _name) noexcept { + return "'" + _name + "'"; +} + // ---------------------------------------------------------------------------- std::string aggregation_to_sql( @@ -254,6 +273,21 @@ std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept { }; std::stringstream stream; + + for (const auto& [enum_name, enum_values] : get_enum_types(_stmt)) { + if (_stmt.if_not_exists) { + stream << "DO $$ BEGIN "; + } + stream << "CREATE TYPE " << enum_name << " AS ENUM (" + << internal::strings::join( + ", ", internal::collect::vector( + enum_values | transform(wrap_in_single_quotes))) + << "); "; + if (_stmt.if_not_exists) { + stream << "EXCEPTION WHEN duplicate_object THEN NULL; END $$;"; + } + } + stream << "CREATE TABLE "; if (_stmt.if_not_exists) { @@ -385,6 +419,20 @@ std::vector get_primary_keys( transform(wrap_in_quotes)); } +std::vector>> get_enum_types( + const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + const auto is_enum = [](const dynamic::Column& _col) -> bool { + return _col.type.visit([&](const auto& _t) -> bool { + using T = std::remove_cvref_t; + return std::is_same_v; + }); + }; + return internal::collect::vector(_stmt.columns | filter(is_enum) | + transform(get_enum_mapping)); +} + std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { using namespace std::ranges::views; @@ -751,7 +799,8 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept { } else if constexpr (std::is_same_v || std::is_same_v) { return "BIGINT"; - + } else if constexpr (std::is_same_v) { + return _t.name; } else if constexpr (std::is_same_v || std::is_same_v) { return "NUMERIC"; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 472c902..4802233 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -769,7 +769,8 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept { } else if constexpr (std::is_same_v) { return _t.type_name; - + } else if constexpr (std::is_same_v) { + return "TEXT"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } diff --git a/tests/mysql/test_enum_crosstable.cpp b/tests/mysql/test_enum_crosstable.cpp new file mode 100644 index 0000000..dbbcead --- /dev/null +++ b/tests/mysql/test_enum_crosstable.cpp @@ -0,0 +1,107 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_cross_table { +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Employee { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + AccessRestriction access_level; +}; + +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(mysql, test_enum_cross_table) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto employees = std::vector({ + Employee{.first_name = "Homer", + .last_name = "Simpson", + .access_level = AccessRestriction::PUBLIC}, + Employee{.first_name = "Waylon", + .last_name = "Smithers", + .access_level = AccessRestriction::INTERNAL}, + Employee{.first_name = "Montgomery", + .last_name = "Burns", + .access_level = AccessRestriction::CONFIDENTIAL}, + }); + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + const auto conn = mysql::connect(credentials); + conn.and_then(drop | if_exists) + .and_then(drop | if_exists); + + write(conn, employees); + write(conn, documents); + + const auto smithers = conn.and_then(sqlgen::read | + where("last_name"_c == "Smithers" and + "first_name"_c == "Waylon")) + .value(); + + const auto smithers_level = smithers.access_level; + const auto smithers_documents = + conn.and_then(sqlgen::read> | + where("min_access_level"_c == smithers_level || + "min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c)) + .value(); + + const auto expected_ids = std::set{1, 2, 4, 5, 7, 9}; + std::set actual_ids; + for (const auto &d : smithers_documents) { + actual_ids.emplace(d.id()); + } + + EXPECT_EQ(expected_ids, actual_ids); +} + +} // namespace test_enum_cross_table + +#endif diff --git a/tests/mysql/test_enum_lookup.cpp b/tests/mysql/test_enum_lookup.cpp new file mode 100644 index 0000000..6c8b89e --- /dev/null +++ b/tests/mysql/test_enum_lookup.cpp @@ -0,0 +1,91 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_lookup { + +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(mysql, test_enum_lookup) { + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto public_documents = + mysql::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(documents))) + .and_then(sqlgen::read> | + where("min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c.desc())) + .value(); + + const auto expected = std::vector({ + Document{.id = 7, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.id = 4, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.id = 1, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + }); + + const auto json1 = rfl::json::write(expected); + const auto json2 = rfl::json::write(public_documents); + EXPECT_EQ(json1, json2); +} + +} // namespace test_enum_lookup + +#endif diff --git a/tests/mysql/test_enum_namespace.cpp b/tests/mysql/test_enum_namespace.cpp new file mode 100644 index 0000000..28ff5f7 --- /dev/null +++ b/tests/mysql/test_enum_namespace.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_namespace { +namespace first { +enum class IdenticallyNamed { VALUE0, VALUE1, VALUE2 }; + +} +namespace second { +enum class IdenticallyNamed { VALUE3, VALUE4, VALUE5 }; +} + +struct MultiStruct { + sqlgen::PrimaryKey id; + first::IdenticallyNamed enum_one; + second::IdenticallyNamed enum_two; +}; + +TEST(mysql, test_enum_namespace) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto objects = std::vector({ + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE0, + .enum_two = second::IdenticallyNamed::VALUE3}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE1, + .enum_two = second::IdenticallyNamed::VALUE4}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE2, + .enum_two = second::IdenticallyNamed::VALUE5}, + }); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + const auto conn = mysql::connect(credentials); + conn.and_then(drop | if_exists); + + write(conn, objects); + + const auto read_objects = + sqlgen::read>(conn).value(); + std::vector actual_ids; + for (const auto& obj : read_objects) { + if (obj.enum_one == first::IdenticallyNamed::VALUE0) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE3); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE1) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE4); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE2) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE5); + } else { + FAIL() << "Unexpected enum value"; + } + } +} + +} // namespace test_enum_namespace + +#endif diff --git a/tests/postgres/test_enum_crosstable.cpp b/tests/postgres/test_enum_crosstable.cpp new file mode 100644 index 0000000..460cb6e --- /dev/null +++ b/tests/postgres/test_enum_crosstable.cpp @@ -0,0 +1,108 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_cross_table { +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Employee { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + AccessRestriction access_level; +}; + +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(postgres, test_enum_cross_table) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto employees = std::vector({ + Employee{.first_name = "Homer", + .last_name = "Simpson", + .access_level = AccessRestriction::PUBLIC}, + Employee{.first_name = "Waylon", + .last_name = "Smithers", + .access_level = AccessRestriction::INTERNAL}, + Employee{.first_name = "Montgomery", + .last_name = "Burns", + .access_level = AccessRestriction::CONFIDENTIAL}, + }); + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + const auto conn = postgres::connect(credentials); + conn.and_then(drop | if_exists) + .and_then(drop | if_exists); + + write(conn, employees); + write(conn, documents); + + const auto smithers = conn.and_then(sqlgen::read | + where("last_name"_c == "Smithers" and + "first_name"_c == "Waylon")) + .value(); + // EXPECT_FALSE(smithers.empty()); + + const auto smithers_level = smithers.access_level; + const auto smithers_documents = + conn.and_then(sqlgen::read> | + where("min_access_level"_c == smithers_level || + "min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c)) + .value(); + + const auto expected_ids = std::set{1, 2, 4, 5, 7, 9}; + std::set actual_ids; + for (const auto &d : smithers_documents) { + actual_ids.emplace(d.id()); + } + + EXPECT_EQ(expected_ids, actual_ids); +} + +} // namespace test_enum_cross_table + +#endif diff --git a/tests/postgres/test_enum_lookup.cpp b/tests/postgres/test_enum_lookup.cpp new file mode 100644 index 0000000..8a8d5bd --- /dev/null +++ b/tests/postgres/test_enum_lookup.cpp @@ -0,0 +1,91 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_lookup { + +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(postgres, test_enum_lookup) { + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto public_documents = + postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(documents))) + .and_then(sqlgen::read> | + where("min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c.desc())) + .value(); + + const auto expected = std::vector({ + Document{.id = 7, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.id = 4, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.id = 1, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + }); + + const auto json1 = rfl::json::write(expected); + const auto json2 = rfl::json::write(public_documents); + EXPECT_EQ(json1, json2); +} + +} // namespace test_enum_lookup + +#endif diff --git a/tests/postgres/test_enum_namespace.cpp b/tests/postgres/test_enum_namespace.cpp new file mode 100644 index 0000000..1eb62ef --- /dev/null +++ b/tests/postgres/test_enum_namespace.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_namespace { +namespace first { +enum class IdenticallyNamed { VALUE0, VALUE1, VALUE2 }; + +} +namespace second { +enum class IdenticallyNamed { VALUE3, VALUE4, VALUE5 }; +} + +struct MultiStruct { + sqlgen::PrimaryKey id; + first::IdenticallyNamed enum_one; + second::IdenticallyNamed enum_two; +}; + +TEST(mysql, test_enum_namespace) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto objects = std::vector({ + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE0, + .enum_two = second::IdenticallyNamed::VALUE3}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE1, + .enum_two = second::IdenticallyNamed::VALUE4}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE2, + .enum_two = second::IdenticallyNamed::VALUE5}, + }); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + const auto conn = postgres::connect(credentials); + conn.and_then(drop | if_exists); + + write(conn, objects); + + const auto read_objects = + sqlgen::read>(conn).value(); + std::vector actual_ids; + for (const auto& obj : read_objects) { + if (obj.enum_one == first::IdenticallyNamed::VALUE0) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE3); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE1) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE4); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE2) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE5); + } else { + FAIL() << "Unexpected enum value"; + } + } +} + +} // namespace test_enum_namespace + +#endif diff --git a/tests/sqlite/test_enum_crosstable.cpp b/tests/sqlite/test_enum_crosstable.cpp new file mode 100644 index 0000000..7024fef --- /dev/null +++ b/tests/sqlite/test_enum_crosstable.cpp @@ -0,0 +1,103 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_cross_table { +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Employee { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + AccessRestriction access_level; +}; + +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(sqlite, test_enum_cross_table) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto employees = std::vector({ + Employee{.first_name = "Homer", + .last_name = "Simpson", + .access_level = AccessRestriction::PUBLIC}, + Employee{.first_name = "Waylon", + .last_name = "Smithers", + .access_level = AccessRestriction::INTERNAL}, + Employee{.first_name = "Montgomery", + .last_name = "Burns", + .access_level = AccessRestriction::CONFIDENTIAL}, + }); + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + const auto conn = sqlite::connect(); + conn.and_then(drop | if_exists) + .and_then(drop | if_exists); + + write(conn, employees); + write(conn, documents); + + const auto smithers = conn.and_then(sqlgen::read | + where("last_name"_c == "Smithers" and + "first_name"_c == "Waylon")) + .value(); + + const auto smithers_level = smithers.access_level; + const auto smithers_documents = + conn.and_then(sqlgen::read> | + where("min_access_level"_c == smithers_level || + "min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c)) + .value(); + + const auto expected_ids = std::set{1, 2, 4, 5, 7, 9}; + std::set actual_ids; + for (const auto &d : smithers_documents) { + actual_ids.emplace(d.id()); + } + + EXPECT_EQ(expected_ids, actual_ids); +} + +} // namespace test_enum_cross_table + +#endif diff --git a/tests/sqlite/test_enum_lookup.cpp b/tests/sqlite/test_enum_lookup.cpp new file mode 100644 index 0000000..30325bf --- /dev/null +++ b/tests/sqlite/test_enum_lookup.cpp @@ -0,0 +1,86 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_lookup { + +enum class AccessRestriction { PUBLIC = 1, INTERNAL = 2, CONFIDENTIAL = 3 }; +struct Document { + sqlgen::PrimaryKey id; + AccessRestriction min_access_level; + std::string name; + std::string path; +}; + +TEST(sqlite, test_enum_lookup) { + auto documents = std::vector({ + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Staff Memo", + .path = "/documents/powerplant/staff_memo.txt"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Operations Report", + .path = "/documents/powerplant/operations_report.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Budget Q1", + .path = "/documents/powerplant/budget_q1.pdf"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "HR Policies", + .path = "/documents/powerplant/hr_policies.pdf"}, + Document{.min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.min_access_level = AccessRestriction::CONFIDENTIAL, + .name = "Executive Summary", + .path = "/documents/powerplant/executive_summary.docx"}, + Document{.min_access_level = AccessRestriction::INTERNAL, + .name = "Release Notes", + .path = "/documents/powerplant/release_notes.txt"}, + }); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto public_documents = + sqlite::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(documents))) + .and_then(sqlgen::read> | + where("min_access_level"_c == AccessRestriction::PUBLIC) | + order_by("name"_c.desc())) + .value(); + + const auto expected = std::vector({ + Document{.id = 7, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Team Photo", + .path = "/documents/powerplant/team.jpg"}, + Document{.id = 4, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Project Plan", + .path = "/documents/powerplant/project_plan.md"}, + Document{.id = 1, + .min_access_level = AccessRestriction::PUBLIC, + .name = "Power Plant Safety Manual", + .path = "/documents/powerplant/safety_manual.txt"}, + }); + + const auto json1 = rfl::json::write(expected); + const auto json2 = rfl::json::write(public_documents); + EXPECT_EQ(json1, json2); +} + +} // namespace test_enum_lookup + +#endif diff --git a/tests/sqlite/test_enum_namespace.cpp b/tests/sqlite/test_enum_namespace.cpp new file mode 100644 index 0000000..00500ee --- /dev/null +++ b/tests/sqlite/test_enum_namespace.cpp @@ -0,0 +1,62 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_enum_namespace { +namespace first { +enum class IdenticallyNamed { VALUE0, VALUE1, VALUE2 }; + +} +namespace second { +enum class IdenticallyNamed { VALUE3, VALUE4, VALUE5 }; +} + +struct MultiStruct { + sqlgen::PrimaryKey id; + first::IdenticallyNamed enum_one; + second::IdenticallyNamed enum_two; +}; + +TEST(mysql, test_enum_namespace) { + using namespace sqlgen; + using namespace sqlgen::literals; + + auto objects = std::vector({ + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE0, + .enum_two = second::IdenticallyNamed::VALUE3}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE1, + .enum_two = second::IdenticallyNamed::VALUE4}, + MultiStruct{.enum_one = first::IdenticallyNamed::VALUE2, + .enum_two = second::IdenticallyNamed::VALUE5}, + }); + + const auto conn = sqlite::connect(); + conn.and_then(drop | if_exists); + + write(conn, objects); + + const auto read_objects = + sqlgen::read>(conn).value(); + std::vector actual_ids; + for (const auto& obj : read_objects) { + if (obj.enum_one == first::IdenticallyNamed::VALUE0) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE3); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE1) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE4); + } else if (obj.enum_one == first::IdenticallyNamed::VALUE2) { + EXPECT_EQ(obj.enum_two, second::IdenticallyNamed::VALUE5); + } else { + FAIL() << "Unexpected enum value"; + } + } +} + +} // namespace test_enum_namespace + +#endif