diff --git a/docs/primary_key.md b/docs/primary_key.md index affd635..9072c4b 100644 --- a/docs/primary_key.md +++ b/docs/primary_key.md @@ -27,6 +27,57 @@ CREATE TABLE IF NOT EXISTS "People"( ); ``` +### Auto-incrementing Primary Keys + +You can define an auto-incrementing primary key by providing `sqlgen::auto_incr` as the second template argument to `sqlgen::PrimaryKey`. The underlying type of an auto-incrementing primary key must be an integral type. + +```cpp +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; +``` + +This will produce SQL schema with an auto-incrementing primary key. For instance, for PostgreSQL it will generate: + +```sql +CREATE TABLE IF NOT EXISTS "Person"( + "id" INTEGER GENERATED ALWAYS AS IDENTITY, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "age" INTEGER NOT NULL, + PRIMARY KEY("id") +); +``` + +And for SQLite: + +```sql +CREATE TABLE IF NOT EXISTS "Person"( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "age" INTEGER NOT NULL +); +``` + +When you insert an object with an auto-incrementing primary key, you do not need to provide a value for the key field. The database will automatically assign a unique, incrementing value. + +```cpp +auto homer = Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}; +// The 'id' field is not set. + +// After writing to the database and reading it back, the 'id' will be populated. +auto people = std::vector({homer}); +auto result = conn.and_then(sqlgen::write(std::ref(people))) + .and_then(sqlgen::read>()) + .value(); + +// result[0].id will now have a value, for instance 1. +``` + ### Assignment and Access Assign values to primary key fields: @@ -51,6 +102,7 @@ person.first_name.value(); - The template parameter specifies the type of the primary key field - Primary key fields are automatically marked as NOT NULL in the generated SQL +- Auto-incrementing primary keys must have an integral type. - The class supports: - Direct value assignment - Multiple access methods for the underlying value diff --git a/include/sqlgen/PrimaryKey.hpp b/include/sqlgen/PrimaryKey.hpp index 92b4681..b5b5831 100644 --- a/include/sqlgen/PrimaryKey.hpp +++ b/include/sqlgen/PrimaryKey.hpp @@ -1,30 +1,40 @@ #ifndef SQLGEN_PRIMARY_KEY_HPP_ #define SQLGEN_PRIMARY_KEY_HPP_ +#include + #include "transpilation/is_nullable.hpp" namespace sqlgen { -template +inline constexpr bool auto_incr = true; + +template struct PrimaryKey { using ReflectionType = T; + static constexpr bool auto_incr = _auto_incr; - static_assert(!transpilation::is_nullable_v, - "A primary key cannot be nullable."); + static_assert( + !transpilation::is_nullable_v, + "A primary key cannot be nullable. Please use a non-nullable type."); + static_assert(!_auto_incr || std::is_integral_v, + "The type of an auto-incrementing primary key must be " + "integral. Please use an integral type or remove auto_incr."); - PrimaryKey() : value_(0) {} + PrimaryKey() : value_(T()) {} PrimaryKey(const T& _value) : value_(_value) {} - PrimaryKey(PrimaryKey&& _other) noexcept = default; + PrimaryKey(PrimaryKey&& _other) noexcept = default; - PrimaryKey(const PrimaryKey& _other) = default; + PrimaryKey(const PrimaryKey& _other) = default; - template - PrimaryKey(const PrimaryKey& _other) : value_(_other.get()) {} + template + PrimaryKey(const PrimaryKey& _other) + : value_(_other.get()) {} - template - PrimaryKey(PrimaryKey&& _other) : value_(_other.get()) {} + template + PrimaryKey(PrimaryKey&& _other) : value_(_other.get()) {} template , @@ -71,22 +81,22 @@ struct PrimaryKey { } /// Assigns the underlying object. - PrimaryKey& operator=(const PrimaryKey& _other) = default; + PrimaryKey& operator=(const PrimaryKey& _other) = default; /// Assigns the underlying object. - PrimaryKey& operator=(PrimaryKey&& _other) = default; + PrimaryKey& operator=(PrimaryKey&& _other) = default; /// Assigns the underlying object. - template - auto& operator=(const PrimaryKey& _other) { + template + auto& operator=(const PrimaryKey& _other) { value_ = _other.get(); return *this; } /// Assigns the underlying object. - template - auto& operator=(PrimaryKey&& _other) { - value_ = std::forward(_other.value_); + template + auto& operator=(PrimaryKey&& _other) { + value_ = std::move(_other.value_); return *this; } diff --git a/include/sqlgen/dynamic/types.hpp b/include/sqlgen/dynamic/types.hpp index 67c1f3a..a5d99e6 100644 --- a/include/sqlgen/dynamic/types.hpp +++ b/include/sqlgen/dynamic/types.hpp @@ -7,6 +7,7 @@ namespace sqlgen::dynamic::types { struct Properties { + bool auto_incr = false; bool primary = false; bool nullable = false; }; diff --git a/include/sqlgen/internal/remove_auto_incr_primary_t.hpp b/include/sqlgen/internal/remove_auto_incr_primary_t.hpp new file mode 100644 index 0000000..0dec1a5 --- /dev/null +++ b/include/sqlgen/internal/remove_auto_incr_primary_t.hpp @@ -0,0 +1,60 @@ +#ifndef SQLGEN_INTERNAL_REMOVE_AUTO_INCR_PRIMARY_HPP_ +#define SQLGEN_INTERNAL_REMOVE_AUTO_INCR_PRIMARY_HPP_ + +#include +#include + +#include "../PrimaryKey.hpp" +#include "../transpilation/is_primary_key.hpp" + +namespace sqlgen::internal { + +namespace remove_auto_incr { + +template +struct FieldWrapper {}; + +template +struct NamedTupleWrapper; + +template +struct NamedTupleWrapper> { + using Type = rfl::NamedTuple; + + template + friend constexpr auto operator+(const NamedTupleWrapper&, + const FieldWrapper&) { + if constexpr (transpilation::is_primary_key_v< + std::remove_pointer_t>) { + if constexpr (std::remove_pointer_t::auto_incr) { + return NamedTupleWrapper>{}; + } else { + return NamedTupleWrapper>{}; + } + } else { + return NamedTupleWrapper>{}; + } + } +}; + +template +struct RemoveAutoIncrPrimary; + +template +struct RemoveAutoIncrPrimary> { + static constexpr auto wrapper = + (NamedTupleWrapper>{} + ... + FieldWrapper{}); + + using Type = decltype(wrapper)::Type; +}; + +} // namespace remove_auto_incr + +template +using remove_auto_incr_primary_t = + typename remove_auto_incr::RemoveAutoIncrPrimary< + std::remove_cvref_t>::Type; + +} // namespace sqlgen::internal + +#endif diff --git a/include/sqlgen/internal/to_str_vec.hpp b/include/sqlgen/internal/to_str_vec.hpp index c8cd0dd..74e4d2a 100644 --- a/include/sqlgen/internal/to_str_vec.hpp +++ b/include/sqlgen/internal/to_str_vec.hpp @@ -7,17 +7,20 @@ #include #include +#include "remove_auto_incr_primary_t.hpp" #include "to_str.hpp" namespace sqlgen::internal { template std::vector> to_str_vec(const T& _t) { + const auto view = rfl::to_view(_t); + using ViewType = remove_auto_incr_primary_t; return rfl::apply( [](auto... _ptrs) { return std::vector>({to_str(*_ptrs)...}); }, - rfl::to_view(_t).values()); + ViewType(view).values()); } } // namespace sqlgen::internal diff --git a/include/sqlgen/parsing/Parser_primary_key.hpp b/include/sqlgen/parsing/Parser_primary_key.hpp index 4e1aa91..457a169 100644 --- a/include/sqlgen/parsing/Parser_primary_key.hpp +++ b/include/sqlgen/parsing/Parser_primary_key.hpp @@ -11,23 +11,29 @@ namespace sqlgen::parsing { -template -struct Parser> { - static Result> read( +template +struct Parser> { + static Result> read( const std::optional& _str) noexcept { return Parser>::read(_str).transform( - [](auto&& _t) -> PrimaryKey { - return PrimaryKey(std::move(_t)); + [](auto&& _t) -> PrimaryKey { + return PrimaryKey(std::move(_t)); }); } - static std::optional write(const PrimaryKey& _p) noexcept { - return Parser>::write(_p.value()); + static std::optional write( + const PrimaryKey& _p) noexcept { + if constexpr (_auto_incr) { + return std::nullopt; + } else { + return Parser>::write(_p.value()); + } } static dynamic::Type to_type() noexcept { return Parser>::to_type().visit( [](auto _t) -> dynamic::Type { + _t.properties.auto_incr = _auto_incr; _t.properties.primary = true; return _t; }); diff --git a/include/sqlgen/transpilation/is_primary_key.hpp b/include/sqlgen/transpilation/is_primary_key.hpp index ff750bb..cf21ff4 100644 --- a/include/sqlgen/transpilation/is_primary_key.hpp +++ b/include/sqlgen/transpilation/is_primary_key.hpp @@ -13,8 +13,8 @@ class is_primary_key; template class is_primary_key : public std::false_type {}; -template -class is_primary_key> : public std::true_type {}; +template +class is_primary_key> : public std::true_type {}; template constexpr bool is_primary_key_v = is_primary_key>::value; diff --git a/include/sqlgen/transpilation/to_create_table.hpp b/include/sqlgen/transpilation/to_create_table.hpp index cb2be3d..81886d2 100644 --- a/include/sqlgen/transpilation/to_create_table.hpp +++ b/include/sqlgen/transpilation/to_create_table.hpp @@ -9,6 +9,7 @@ #include "../dynamic/CreateTable.hpp" #include "../dynamic/Table.hpp" +#include "../internal/remove_auto_incr_primary_t.hpp" #include "get_schema.hpp" #include "get_tablename.hpp" #include "make_columns.hpp" diff --git a/include/sqlgen/transpilation/to_insert_or_write.hpp b/include/sqlgen/transpilation/to_insert_or_write.hpp index 4eb31aa..ab2993b 100644 --- a/include/sqlgen/transpilation/to_insert_or_write.hpp +++ b/include/sqlgen/transpilation/to_insert_or_write.hpp @@ -10,6 +10,7 @@ #include "../dynamic/Table.hpp" #include "../internal/collect/vector.hpp" +#include "../internal/remove_auto_incr_primary_t.hpp" #include "get_schema.hpp" #include "get_tablename.hpp" #include "make_columns.hpp" @@ -22,7 +23,8 @@ template InsertOrWrite to_insert_or_write() { using namespace std::ranges::views; - using NamedTupleType = rfl::named_tuple_t>; + using NamedTupleType = sqlgen::internal::remove_auto_incr_primary_t< + rfl::named_tuple_t>>; using Fields = typename NamedTupleType::Fields; const auto columns = make_columns( diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 99cda64..7cf790d 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -12,9 +12,6 @@ namespace sqlgen::postgres { -std::string add_not_null_if_necessary( - const dynamic::types::Properties& _p) noexcept; - std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept; @@ -46,6 +43,8 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept; std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept; +std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept; + std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; std::string type_to_sql(const dynamic::Type& _type) noexcept; @@ -64,11 +63,6 @@ inline std::string wrap_in_quotes(const std::string& _name) noexcept { // ---------------------------------------------------------------------------- -std::string add_not_null_if_necessary( - const dynamic::types::Properties& _p) noexcept { - return std::string(_p.nullable ? "" : " NOT NULL"); -} - std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { @@ -188,7 +182,7 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { std::string column_to_sql_definition(const dynamic::Column& _col) noexcept { return wrap_in_quotes(_col.name) + " " + type_to_sql(_col.type) + - add_not_null_if_necessary( + properties_to_sql( _col.type.visit([](const auto& _t) { return _t.properties; })); } @@ -489,6 +483,16 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { }); } +std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept { + if (_p.auto_incr) { + return " GENERATED ALWAYS AS IDENTITY"; + } else if (!_p.nullable) { + return " NOT NULL"; + } else { + return ""; + } +} + std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { using namespace std::ranges::views; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index d55b45e..befdf72 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -451,6 +451,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept { return std::string(_p.primary ? " PRIMARY KEY" : "") + + std::string(_p.auto_incr ? " AUTOINCREMENT" : "") + std::string(_p.nullable ? "" : " NOT NULL"); } diff --git a/tests/postgres/test_auto_incr_primary_key.cpp b/tests/postgres/test_auto_incr_primary_key.cpp new file mode 100644 index 0000000..65e7014 --- /dev/null +++ b/tests/postgres/test_auto_incr_primary_key.cpp @@ -0,0 +1,54 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_auto_incr_primary_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_auto_incr_primary_key) { + auto people1 = std::vector( + {Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{.first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto people2 = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + order_by("age"_c.desc())) + .value(); + + people1.at(0).id = 1; + people1.at(1).id = 2; + people1.at(2).id = 3; + people1.at(3).id = 4; + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_auto_incr_primary_key + +#endif diff --git a/tests/sqlite/test_auto_incr_primary_key.cpp b/tests/sqlite/test_auto_incr_primary_key.cpp new file mode 100644 index 0000000..8e8d312 --- /dev/null +++ b/tests/sqlite/test_auto_incr_primary_key.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_auto_incr_primary_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_auto_incr_primary_key) { + auto people1 = std::vector( + {Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{.first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + using namespace sqlgen; + + const auto people2 = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + order_by("age"_c.desc())) + .value(); + + people1.at(0).id = 1; + people1.at(1).id = 2; + people1.at(2).id = 3; + people1.at(3).id = 4; + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_auto_incr_primary_key