mirror of
https://github.com/getml/sqlgen.git
synced 2026-01-04 08:30:30 -06:00
Add auto-incrementing primary keys (#23)
This commit is contained in:
committed by
GitHub
parent
e4b821138a
commit
aa1a96f1c0
@@ -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<uint32_t, sqlgen::auto_incr> 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<Person>({homer});
|
||||
auto result = conn.and_then(sqlgen::write(std::ref(people)))
|
||||
.and_then(sqlgen::read<std::vector<Person>>())
|
||||
.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
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
#ifndef SQLGEN_PRIMARY_KEY_HPP_
|
||||
#define SQLGEN_PRIMARY_KEY_HPP_
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "transpilation/is_nullable.hpp"
|
||||
|
||||
namespace sqlgen {
|
||||
|
||||
template <class T>
|
||||
inline constexpr bool auto_incr = true;
|
||||
|
||||
template <class T, bool _auto_incr = false>
|
||||
struct PrimaryKey {
|
||||
using ReflectionType = T;
|
||||
static constexpr bool auto_incr = _auto_incr;
|
||||
|
||||
static_assert(!transpilation::is_nullable_v<T>,
|
||||
"A primary key cannot be nullable.");
|
||||
static_assert(
|
||||
!transpilation::is_nullable_v<T>,
|
||||
"A primary key cannot be nullable. Please use a non-nullable type.");
|
||||
static_assert(!_auto_incr || std::is_integral_v<T>,
|
||||
"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<T>&& _other) noexcept = default;
|
||||
PrimaryKey(PrimaryKey&& _other) noexcept = default;
|
||||
|
||||
PrimaryKey(const PrimaryKey<T>& _other) = default;
|
||||
PrimaryKey(const PrimaryKey& _other) = default;
|
||||
|
||||
template <class U>
|
||||
PrimaryKey(const PrimaryKey<U>& _other) : value_(_other.get()) {}
|
||||
template <class U, bool _other_auto_incr>
|
||||
PrimaryKey(const PrimaryKey<U, _other_auto_incr>& _other)
|
||||
: value_(_other.get()) {}
|
||||
|
||||
template <class U>
|
||||
PrimaryKey(PrimaryKey<U>&& _other) : value_(_other.get()) {}
|
||||
template <class U, bool _other_auto_incr>
|
||||
PrimaryKey(PrimaryKey<U, _other_auto_incr>&& _other) : value_(_other.get()) {}
|
||||
|
||||
template <class U,
|
||||
typename std::enable_if<std::is_convertible_v<U, ReflectionType>,
|
||||
@@ -71,22 +81,22 @@ struct PrimaryKey {
|
||||
}
|
||||
|
||||
/// Assigns the underlying object.
|
||||
PrimaryKey<T>& operator=(const PrimaryKey<T>& _other) = default;
|
||||
PrimaryKey& operator=(const PrimaryKey& _other) = default;
|
||||
|
||||
/// Assigns the underlying object.
|
||||
PrimaryKey<T>& operator=(PrimaryKey<T>&& _other) = default;
|
||||
PrimaryKey& operator=(PrimaryKey&& _other) = default;
|
||||
|
||||
/// Assigns the underlying object.
|
||||
template <class U>
|
||||
auto& operator=(const PrimaryKey<U>& _other) {
|
||||
template <class U, bool _other_auto_incr>
|
||||
auto& operator=(const PrimaryKey<U, _other_auto_incr>& _other) {
|
||||
value_ = _other.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Assigns the underlying object.
|
||||
template <class U>
|
||||
auto& operator=(PrimaryKey<U>&& _other) {
|
||||
value_ = std::forward<T>(_other.value_);
|
||||
template <class U, bool _other_auto_incr>
|
||||
auto& operator=(PrimaryKey<U, _other_auto_incr>&& _other) {
|
||||
value_ = std::move(_other.value_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
namespace sqlgen::dynamic::types {
|
||||
|
||||
struct Properties {
|
||||
bool auto_incr = false;
|
||||
bool primary = false;
|
||||
bool nullable = false;
|
||||
};
|
||||
|
||||
60
include/sqlgen/internal/remove_auto_incr_primary_t.hpp
Normal file
60
include/sqlgen/internal/remove_auto_incr_primary_t.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef SQLGEN_INTERNAL_REMOVE_AUTO_INCR_PRIMARY_HPP_
|
||||
#define SQLGEN_INTERNAL_REMOVE_AUTO_INCR_PRIMARY_HPP_
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <type_traits>
|
||||
|
||||
#include "../PrimaryKey.hpp"
|
||||
#include "../transpilation/is_primary_key.hpp"
|
||||
|
||||
namespace sqlgen::internal {
|
||||
|
||||
namespace remove_auto_incr {
|
||||
|
||||
template <class FieldType>
|
||||
struct FieldWrapper {};
|
||||
|
||||
template <class NamedTupleType>
|
||||
struct NamedTupleWrapper;
|
||||
|
||||
template <class... Fields>
|
||||
struct NamedTupleWrapper<rfl::NamedTuple<Fields...>> {
|
||||
using Type = rfl::NamedTuple<Fields...>;
|
||||
|
||||
template <class NewField>
|
||||
friend constexpr auto operator+(const NamedTupleWrapper&,
|
||||
const FieldWrapper<NewField>&) {
|
||||
if constexpr (transpilation::is_primary_key_v<
|
||||
std::remove_pointer_t<typename NewField::Type>>) {
|
||||
if constexpr (std::remove_pointer_t<typename NewField::Type>::auto_incr) {
|
||||
return NamedTupleWrapper<rfl::NamedTuple<Fields...>>{};
|
||||
} else {
|
||||
return NamedTupleWrapper<rfl::NamedTuple<Fields..., NewField>>{};
|
||||
}
|
||||
} else {
|
||||
return NamedTupleWrapper<rfl::NamedTuple<Fields..., NewField>>{};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class NamedTupleType>
|
||||
struct RemoveAutoIncrPrimary;
|
||||
|
||||
template <class... Fields>
|
||||
struct RemoveAutoIncrPrimary<rfl::NamedTuple<Fields...>> {
|
||||
static constexpr auto wrapper =
|
||||
(NamedTupleWrapper<rfl::NamedTuple<>>{} + ... + FieldWrapper<Fields>{});
|
||||
|
||||
using Type = decltype(wrapper)::Type;
|
||||
};
|
||||
|
||||
} // namespace remove_auto_incr
|
||||
|
||||
template <class NamedTupleType>
|
||||
using remove_auto_incr_primary_t =
|
||||
typename remove_auto_incr::RemoveAutoIncrPrimary<
|
||||
std::remove_cvref_t<NamedTupleType>>::Type;
|
||||
|
||||
} // namespace sqlgen::internal
|
||||
|
||||
#endif
|
||||
@@ -7,17 +7,20 @@
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "remove_auto_incr_primary_t.hpp"
|
||||
#include "to_str.hpp"
|
||||
|
||||
namespace sqlgen::internal {
|
||||
|
||||
template <class T>
|
||||
std::vector<std::optional<std::string>> to_str_vec(const T& _t) {
|
||||
const auto view = rfl::to_view(_t);
|
||||
using ViewType = remove_auto_incr_primary_t<decltype(view)>;
|
||||
return rfl::apply(
|
||||
[](auto... _ptrs) {
|
||||
return std::vector<std::optional<std::string>>({to_str(*_ptrs)...});
|
||||
},
|
||||
rfl::to_view(_t).values());
|
||||
ViewType(view).values());
|
||||
}
|
||||
|
||||
} // namespace sqlgen::internal
|
||||
|
||||
@@ -11,23 +11,29 @@
|
||||
|
||||
namespace sqlgen::parsing {
|
||||
|
||||
template <class T>
|
||||
struct Parser<PrimaryKey<T>> {
|
||||
static Result<PrimaryKey<T>> read(
|
||||
template <class T, bool _auto_incr>
|
||||
struct Parser<PrimaryKey<T, _auto_incr>> {
|
||||
static Result<PrimaryKey<T, _auto_incr>> read(
|
||||
const std::optional<std::string>& _str) noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::read(_str).transform(
|
||||
[](auto&& _t) -> PrimaryKey<T> {
|
||||
return PrimaryKey<T>(std::move(_t));
|
||||
[](auto&& _t) -> PrimaryKey<T, _auto_incr> {
|
||||
return PrimaryKey<T, _auto_incr>(std::move(_t));
|
||||
});
|
||||
}
|
||||
|
||||
static std::optional<std::string> write(const PrimaryKey<T>& _p) noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::write(_p.value());
|
||||
static std::optional<std::string> write(
|
||||
const PrimaryKey<T, _auto_incr>& _p) noexcept {
|
||||
if constexpr (_auto_incr) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
return Parser<std::remove_cvref_t<T>>::write(_p.value());
|
||||
}
|
||||
}
|
||||
|
||||
static dynamic::Type to_type() noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::to_type().visit(
|
||||
[](auto _t) -> dynamic::Type {
|
||||
_t.properties.auto_incr = _auto_incr;
|
||||
_t.properties.primary = true;
|
||||
return _t;
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ class is_primary_key;
|
||||
template <class T>
|
||||
class is_primary_key : public std::false_type {};
|
||||
|
||||
template <class T>
|
||||
class is_primary_key<PrimaryKey<T>> : public std::true_type {};
|
||||
template <class T, bool _auto_incr>
|
||||
class is_primary_key<PrimaryKey<T, _auto_incr>> : public std::true_type {};
|
||||
|
||||
template <class T>
|
||||
constexpr bool is_primary_key_v = is_primary_key<std::remove_cvref_t<T>>::value;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 <class T, class InsertOrWrite>
|
||||
InsertOrWrite to_insert_or_write() {
|
||||
using namespace std::ranges::views;
|
||||
|
||||
using NamedTupleType = rfl::named_tuple_t<std::remove_cvref_t<T>>;
|
||||
using NamedTupleType = sqlgen::internal::remove_auto_incr_primary_t<
|
||||
rfl::named_tuple_t<std::remove_cvref_t<T>>>;
|
||||
using Fields = typename NamedTupleType::Fields;
|
||||
|
||||
const auto columns = make_columns<Fields>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
54
tests/postgres/test_auto_incr_primary_key.cpp
Normal file
54
tests/postgres/test_auto_incr_primary_key.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/postgres.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_auto_incr_primary_key {
|
||||
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
|
||||
std::string first_name;
|
||||
std::string last_name;
|
||||
int age;
|
||||
};
|
||||
|
||||
TEST(postgres, test_auto_incr_primary_key) {
|
||||
auto people1 = std::vector<Person>(
|
||||
{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<Person> | if_exists)
|
||||
.and_then(write(std::ref(people1)))
|
||||
.and_then(sqlgen::read<std::vector<Person>> |
|
||||
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
|
||||
44
tests/sqlite/test_auto_incr_primary_key.cpp
Normal file
44
tests/sqlite/test_auto_incr_primary_key.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/sqlite.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_auto_incr_primary_key {
|
||||
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t, sqlgen::auto_incr> id;
|
||||
std::string first_name;
|
||||
std::string last_name;
|
||||
int age;
|
||||
};
|
||||
|
||||
TEST(sqlite, test_auto_incr_primary_key) {
|
||||
auto people1 = std::vector<Person>(
|
||||
{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<std::vector<Person>> |
|
||||
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
|
||||
Reference in New Issue
Block a user