Add auto-incrementing primary keys (#23)

This commit is contained in:
Dr. Patrick Urbanke (劉自成)
2025-06-24 22:34:50 +02:00
committed by GitHub
parent e4b821138a
commit aa1a96f1c0
13 changed files with 275 additions and 37 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -7,6 +7,7 @@
namespace sqlgen::dynamic::types {
struct Properties {
bool auto_incr = false;
bool primary = false;
bool nullable = false;
};

View 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

View File

@@ -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

View File

@@ -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;
});

View File

@@ -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;

View File

@@ -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"

View File

@@ -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>(

View File

@@ -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;

View File

@@ -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");
}

View 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

View 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