diff --git a/docs/README.md b/docs/README.md index 16353ce..a412d4e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,6 +39,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab ## Data Types and Validation +- [sqlgen::ForeignKey](foreign_key.md) - How to establish referential integrity between tables - [sqlgen::Pattern](pattern.md) - How to add regex pattern validation to avoid SQL injection - [sqlgen::Timestamp](timestamp.md) - How timestamps work in sqlgen - [sqlgen::Varchar](varchar.md) - How varchars work in sqlgen diff --git a/docs/foreign_key.md b/docs/foreign_key.md new file mode 100644 index 0000000..288e75d --- /dev/null +++ b/docs/foreign_key.md @@ -0,0 +1,166 @@ +# `sqlgen::ForeignKey` + +`sqlgen::ForeignKey` is used to establish referential integrity between tables by creating foreign key relationships. It ensures that values in one table reference valid values in another table's primary key. + +## Usage + +### Basic Definition + +Define a foreign key field in your struct by specifying the type, the referenced table type, and the column name: + +```cpp +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; +``` + +This generates the following SQL schema: + +```sql +CREATE TABLE IF NOT EXISTS "Person"( + "id" INTEGER NOT NULL, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "age" INTEGER NOT NULL, + PRIMARY KEY("id") +); + +CREATE TABLE IF NOT EXISTS "Relationship"( + "parent_id" INTEGER NOT NULL REFERENCES "Person"("id"), + "child_id" INTEGER NOT NULL +); +``` + +### Template Parameters + +The `sqlgen::ForeignKey` template takes three parameters: + +1. **T**: The type of the foreign key field (must match the referenced column's type) +2. **ForeignTableType**: The struct type of the table being referenced +3. **col_name**: The name of the column in the referenced table (as a string literal) + +```cpp +sqlgen::ForeignKey field_name; +``` + +**Important**: The referenced column must be a primary key in the foreign table. + +### Type Safety and Validation + +`sqlgen::ForeignKey` provides compile-time validation to ensure: + +1. **Column Existence**: The referenced column must exist in the foreign table +2. **Primary Key Constraint**: The referenced column must be a primary key +3. **Type Compatibility**: The foreign key type must match the referenced column's type + +```cpp +struct Person { + sqlgen::PrimaryKey id; + std::string name; +}; + +struct Order { + sqlgen::PrimaryKey id; + // This will compile successfully - "id" exists in Person, is a primary key, and types match + sqlgen::ForeignKey person_id; + + // This would cause a compile error - type mismatch + // sqlgen::ForeignKey person_id; +}; +``` + +### Assignment and Access + +Assign values to foreign key fields: + +```cpp +const auto relationship = Relationship{ + .parent_id = 1, // References Person with id = 1 + .child_id = 2 +}; +``` + +Access the underlying value using any of these methods: + +```cpp +relationship.parent_id(); +relationship.parent_id.get(); +relationship.parent_id.value(); +``` + +### Working with Related Data + +Foreign keys enable you to establish relationships between tables and perform joins: + +```cpp +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; + +// Insert parent records +auto people = std::vector({ + Person{.id = 1, .first_name = "Homer", .last_name = "Simpson"}, + Person{.id = 2, .first_name = "Marge", .last_name = "Simpson"} +}); + +// Insert relationship records that reference the parents +auto relationships = std::vector({ + Relationship{.parent_id = 1, .child_id = 3}, // Homer -> child 3 + Relationship{.parent_id = 1, .child_id = 4}, // Homer -> child 4 + Relationship{.parent_id = 2, .child_id = 3}, // Marge -> child 3 + Relationship{.parent_id = 2, .child_id = 4} // Marge -> child 4 +}); + +// Write both tables to the database +conn.and_then(create_table | if_not_exists) + .and_then(create_table | if_not_exists) + .and_then(insert(std::ref(people))) + .and_then(insert(std::ref(relationships))); +``` + +### Referential Integrity + +Foreign keys enforce referential integrity at the database level: + +- **Insert Validation**: You cannot insert a foreign key value that doesn't exist in the referenced table +- **Delete Protection**: You cannot delete a referenced record without handling the foreign key constraints +- **Update Consistency**: Updates to referenced primary keys are handled according to the database's foreign key rules + +```cpp +// This would fail if Person with id = 999 doesn't exist +auto invalid_relationship = Relationship{ + .parent_id = 999, // This Person doesn't exist + .child_id = 1 +}; +``` + +## Notes + +- The template parameters are: + - `T`: The type of the foreign key field + - `ForeignTableType`: The struct type of the referenced table + - `col_name`: The name of the referenced column (string literal) +- The class supports: + - Direct value assignment + - Multiple access methods for the underlying value + - Reflection for SQL operations + - Move and copy semantics + - Compile-time validation of column existence, primary key constraint, and type compatibility +- Foreign keys can reference any supported SQL data type +- The referenced column must exist in the foreign table, be a primary key, and have a compatible type +- Foreign key relationships are enforced at the database level for data integrity diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index e761558..a35b45c 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -3,6 +3,7 @@ #include "sqlgen/ConnectionPool.hpp" #include "sqlgen/Flatten.hpp" +#include "sqlgen/ForeignKey.hpp" #include "sqlgen/Iterator.hpp" #include "sqlgen/IteratorBase.hpp" #include "sqlgen/Literal.hpp" @@ -17,6 +18,7 @@ #include "sqlgen/aggregations.hpp" #include "sqlgen/as.hpp" #include "sqlgen/begin_transaction.hpp" +#include "sqlgen/cascade.hpp" #include "sqlgen/col.hpp" #include "sqlgen/commit.hpp" #include "sqlgen/create_index.hpp" diff --git a/include/sqlgen/ForeignKey.hpp b/include/sqlgen/ForeignKey.hpp new file mode 100644 index 0000000..1e038af --- /dev/null +++ b/include/sqlgen/ForeignKey.hpp @@ -0,0 +1,113 @@ +#ifndef SQLGEN_FOREIGN_KEY_HPP_ +#define SQLGEN_FOREIGN_KEY_HPP_ + +#include +#include + +#include "Literal.hpp" +#include "transpilation/Col.hpp" +#include "transpilation/all_columns_exist.hpp" +#include "transpilation/is_primary_key.hpp" +#include "transpilation/remove_reflection_t.hpp" +#include "transpilation/underlying_t.hpp" + +namespace sqlgen { + +template +struct ForeignKey { + using ReflectionType = T; + using ForeignTableType = _ForeignTableType; + using ColumnType = Literal<_col_name>; + + static_assert(transpilation::column_exists_v< + _ForeignTableType, + typename rfl::named_tuple_t<_ForeignTableType>::Names, + transpilation::Col<_col_name>>, + "The column referenced to in ForeignKey<...> does not exist on " + "the referenced table."); + static_assert( + std::is_same_v< + std::remove_cvref_t>, + std::remove_cvref_t< + transpilation::remove_reflection_t>>>>, + "The type of the column and the type of the referenced column must be " + "the same."); + static_assert( + transpilation::is_primary_key_v< + std::remove_cvref_t>>, + "The referenced column must be a primary key."); + + ForeignKey() : value_(T()) {} + + ForeignKey(const T& _value) : value_(_value) {} + + ForeignKey(ForeignKey&& _other) noexcept = default; + + ForeignKey(const ForeignKey& _other) = default; + + template , + bool>::type = true> + ForeignKey(const U& _value) : value_(_value) {} + + template , + bool>::type = true> + ForeignKey(U&& _value) noexcept : value_(std::forward(_value)) {} + + ~ForeignKey() = default; + + /// Returns the underlying object. + ReflectionType& get() { return value_; } + + /// Returns the underlying object. + const ReflectionType& get() const { return value_; } + + /// Returns the underlying object. + ReflectionType& operator()() { return value_; } + + /// Returns the underlying object. + const ReflectionType& operator()() const { return value_; } + + /// Assigns the underlying object. + auto& operator=(const ReflectionType& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object. + template , + bool>::type = true> + auto& operator=(const U& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object. + ForeignKey& operator=(const ForeignKey& _other) = default; + + /// Assigns the underlying object. + ForeignKey& operator=(ForeignKey&& _other) = default; + + /// Necessary for the automated transpilation to work. + const T& reflection() const { return value_; } + + /// Assigns the underlying object. + void set(const T& _value) { value_ = _value; } + + /// Returns the underlying object. + T& value() { return value_; } + + /// Returns the underlying object. + const T& value() const { return value_; } + + /// The underlying value. + T value_; +}; + +} // namespace sqlgen + +#endif diff --git a/include/sqlgen/cascade.hpp b/include/sqlgen/cascade.hpp new file mode 100644 index 0000000..3650211 --- /dev/null +++ b/include/sqlgen/cascade.hpp @@ -0,0 +1,19 @@ +#ifndef SQLGEN_CASCADE_HPP_ +#define SQLGEN_CASCADE_HPP_ + +namespace sqlgen { + +struct Cascade {}; + +template +auto operator|(const OtherType& _o, const Cascade&) { + auto o = _o; + o.cascade_ = true; + return o; +} + +inline const auto cascade = Cascade{}; + +} // namespace sqlgen + +#endif diff --git a/include/sqlgen/drop.hpp b/include/sqlgen/drop.hpp index 6b49907..30d0b8b 100644 --- a/include/sqlgen/drop.hpp +++ b/include/sqlgen/drop.hpp @@ -13,8 +13,8 @@ namespace sqlgen { template requires is_connection Result> drop_impl(const Ref& _conn, - const bool _if_exists) { - const auto query = transpilation::to_drop(_if_exists); + const bool _if_exists, const bool _cascade) { + const auto query = transpilation::to_drop(_if_exists, _cascade); return _conn->execute(_conn->to_sql(query)).transform([&](const auto&) { return _conn; }); @@ -23,18 +23,19 @@ Result> drop_impl(const Ref& _conn, template requires is_connection Result> drop_impl(const Result>& _res, - const bool _if_exists) { + const bool _if_exists, const bool _cascade) { return _res.and_then([&](const auto& _conn) { - return drop_impl(_conn, _if_exists); + return drop_impl(_conn, _if_exists, _cascade); }); } template struct Drop { auto operator()(const auto& _conn) const { - return drop_impl(_conn, if_exists_); + return drop_impl(_conn, if_exists_, cascade_); } + bool cascade_ = false; bool if_exists_ = false; }; diff --git a/include/sqlgen/dynamic/Drop.hpp b/include/sqlgen/dynamic/Drop.hpp index fa810f8..05d98d6 100644 --- a/include/sqlgen/dynamic/Drop.hpp +++ b/include/sqlgen/dynamic/Drop.hpp @@ -8,7 +8,8 @@ namespace sqlgen::dynamic { struct Drop { - bool if_exists = true; + bool if_exists = false; + bool cascade = false; Table table; }; diff --git a/include/sqlgen/dynamic/types.hpp b/include/sqlgen/dynamic/types.hpp index b280daf..fd73e23 100644 --- a/include/sqlgen/dynamic/types.hpp +++ b/include/sqlgen/dynamic/types.hpp @@ -2,14 +2,21 @@ #define SQLGEN_DYNAMIC_TABLE_TYPES_HPP_ #include +#include #include namespace sqlgen::dynamic::types { +struct ForeignKeyReference { + std::string table; + std::string column; +}; + struct Properties { bool auto_incr = false; bool primary = false; bool nullable = false; + std::optional foreign_key_reference = std::nullopt; }; // To be used as the default value. diff --git a/include/sqlgen/parsing/Parser.hpp b/include/sqlgen/parsing/Parser.hpp index 5698e84..40fe5dd 100644 --- a/include/sqlgen/parsing/Parser.hpp +++ b/include/sqlgen/parsing/Parser.hpp @@ -3,6 +3,7 @@ #include "Parser_base.hpp" #include "Parser_default.hpp" +#include "Parser_foreign_key.hpp" #include "Parser_optional.hpp" #include "Parser_primary_key.hpp" #include "Parser_shared_ptr.hpp" diff --git a/include/sqlgen/parsing/Parser_foreign_key.hpp b/include/sqlgen/parsing/Parser_foreign_key.hpp new file mode 100644 index 0000000..899044e --- /dev/null +++ b/include/sqlgen/parsing/Parser_foreign_key.hpp @@ -0,0 +1,44 @@ +#ifndef SQLGEN_PARSING_PARSER_FOREIGN_KEY_HPP_ +#define SQLGEN_PARSING_PARSER_FOREIGN_KEY_HPP_ + +#include +#include + +#include "../ForeignKey.hpp" +#include "../Result.hpp" +#include "../dynamic/Type.hpp" +#include "../transpilation/get_tablename.hpp" +#include "Parser_base.hpp" + +namespace sqlgen::parsing { + +template +struct Parser> { + static Result> read( + const std::optional& _str) noexcept { + return Parser>::read(_str).transform([](auto&& _t) { + return ForeignKey(std::move(_t)); + }); + } + + static std::optional write( + const ForeignKey& _f) noexcept { + return Parser>::write(_f.value()); + } + + static dynamic::Type to_type() noexcept { + return Parser>::to_type().visit( + [](auto _t) -> dynamic::Type { + _t.properties.foreign_key_reference = + dynamic::types::ForeignKeyReference{ + .table = transpilation::get_tablename<_ForeignTableType>(), + .column = _col_name.str()}; + return _t; + }); + } +}; + +} // namespace sqlgen::parsing + +#endif diff --git a/include/sqlgen/transpilation/to_drop.hpp b/include/sqlgen/transpilation/to_drop.hpp index 600a92c..66c845b 100644 --- a/include/sqlgen/transpilation/to_drop.hpp +++ b/include/sqlgen/transpilation/to_drop.hpp @@ -18,8 +18,9 @@ namespace sqlgen::transpilation { template requires std::is_class_v> && std::is_aggregate_v> -dynamic::Drop to_drop(const bool _if_exists) { +dynamic::Drop to_drop(const bool _if_exists, const bool _cascade) { return dynamic::Drop{.if_exists = _if_exists, + .cascade = _cascade, .table = dynamic::Table{.name = get_tablename(), .schema = get_schema()}}; } diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index adb0146..ce5a8fd 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -57,7 +57,7 @@ struct ToSQL> { template struct ToSQL> { dynamic::Statement operator()(const auto& _drop) const { - return to_drop(_drop.if_exists_); + return to_drop(_drop.if_exists_, _drop.cascade_); } }; diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index 1f02992..6d6e935 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -43,6 +43,14 @@ std::string escape_single_quote(const std::string& _str) noexcept; std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept; +std::string foreign_keys_to_sql( + const std::vector< + std::pair>& + _foreign_keys) noexcept; + +std::vector> +get_foreign_keys(const dynamic::CreateTable& _stmt) noexcept; + std::vector get_primary_keys( const dynamic::CreateTable& _stmt) noexcept; @@ -320,6 +328,12 @@ std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept { << ")"; } + const auto foreign_keys = get_foreign_keys(_stmt); + + if (foreign_keys.size() != 0) { + stream << ", " << foreign_keys_to_sql(foreign_keys); + } + stream << ");"; return stream.str(); @@ -379,6 +393,10 @@ std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept { } stream << wrap_in_quotes(_stmt.table.name); + if (_stmt.cascade) { + stream << " CASCADE"; + } + stream << ";"; return stream.str(); @@ -400,6 +418,47 @@ std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept { return stream.str(); } +std::string foreign_keys_to_sql( + const std::vector< + std::pair>& + _foreign_keys) noexcept { + using namespace std::ranges::views; + + const auto to_str = + [](const std::pair& + _p) { + return "FOREIGN KEY (" + wrap_in_quotes(_p.first) + ") REFERENCES " + + wrap_in_quotes(_p.second.table) + "(" + + wrap_in_quotes(_p.second.column) + ")"; + }; + + return internal::strings::join( + ", ", internal::collect::vector(_foreign_keys | transform(to_str))); +} + +std::vector> +get_foreign_keys(const dynamic::CreateTable& _stmt) noexcept { + using namespace std::ranges::views; + + const auto get_foreign_key_ref = [](const auto& _col) + -> std::optional { + return _col.type.visit( + [](const auto& _t) { return _t.properties.foreign_key_reference; }); + }; + + const auto has_reference = [&](const auto& _col) -> bool { + return (true && get_foreign_key_ref(_col)); + }; + + const auto to_pair = [&](const auto& _col) + -> std::pair { + return std::make_pair(get_name(_col), get_foreign_key_ref(_col).value()); + }; + + return internal::collect::vector(_stmt.columns | filter(has_reference) | + transform(to_pair)); +} + std::vector get_primary_keys( const dynamic::CreateTable& _stmt) noexcept { using namespace std::ranges::views; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 1770031..81fec8c 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -46,7 +46,8 @@ std::string join_to_sql(const dynamic::Join& _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 properties_to_sql( + const dynamic::types::Properties& _properties) noexcept; std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; @@ -308,6 +309,10 @@ std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept { } stream << wrap_in_quotes(_stmt.table.name); + if (_stmt.cascade) { + stream << " CASCADE"; + } + stream << ";"; return stream.str(); @@ -567,13 +572,22 @@ 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 ""; - } + return [&]() -> std::string { + if (_p.auto_incr) { + return " GENERATED ALWAYS AS IDENTITY"; + } else if (!_p.nullable) { + return " NOT NULL"; + } else { + return ""; + } + }() + [&]() -> std::string { + if (!_p.foreign_key_reference) { + return ""; + } + const auto& ref = *_p.foreign_key_reference; + return " REFERENCES " + wrap_in_quotes(ref.table) + "(" + + wrap_in_quotes(ref.column) + ")"; + }(); } std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 1054647..6f6373d 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -579,9 +579,18 @@ 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"); + return [&]() -> std::string { + return std::string(_p.primary ? " PRIMARY KEY" : "") + + std::string(_p.auto_incr ? " AUTOINCREMENT" : "") + + std::string(_p.nullable ? "" : " NOT NULL"); + }() + [&]() -> std::string { + if (!_p.foreign_key_reference) { + return ""; + } + const auto& ref = *_p.foreign_key_reference; + return " REFERENCES " + wrap_in_quotes(ref.table) + "(" + + wrap_in_quotes(ref.column) + ")"; + }(); } std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { diff --git a/tests/mysql/test_foreign_key.cpp b/tests/mysql/test_foreign_key.cpp new file mode 100644 index 0000000..07c3cb5 --- /dev/null +++ b/tests/mysql/test_foreign_key.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_foreign_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; + +TEST(mysql, test_foreign_key) { + auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto relationships = + std::vector({Relationship{.parent_id = 0, .child_id = 2}, + Relationship{.parent_id = 0, .child_id = 3}, + Relationship{.parent_id = 0, .child_id = 4}, + Relationship{.parent_id = 1, .child_id = 2}, + Relationship{.parent_id = 1, .child_id = 3}, + Relationship{.parent_id = 1, .child_id = 4}}); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = mysql::connect(credentials) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(begin_transaction) + .and_then(create_table) + .and_then(create_table) + .and_then(insert(std::ref(people1))) + .and_then(insert(std::ref(relationships))) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(commit) + .value(); +} + +} // namespace test_foreign_key + +#endif diff --git a/tests/postgres/test_foreign_key.cpp b/tests/postgres/test_foreign_key.cpp new file mode 100644 index 0000000..22f17e6 --- /dev/null +++ b/tests/postgres/test_foreign_key.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_foreign_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; + +TEST(postgres, test_foreign_key) { + auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto relationships = + std::vector({Relationship{.parent_id = 0, .child_id = 2}, + Relationship{.parent_id = 0, .child_id = 3}, + Relationship{.parent_id = 0, .child_id = 4}, + Relationship{.parent_id = 1, .child_id = 2}, + Relationship{.parent_id = 1, .child_id = 3}, + Relationship{.parent_id = 1, .child_id = 4}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists | cascade) + .and_then(drop | if_exists) + .and_then(begin_transaction) + .and_then(create_table) + .and_then(create_table) + .and_then(insert(std::ref(people1))) + .and_then(insert(std::ref(relationships))) + .and_then(drop | if_exists | cascade) + .and_then(drop | if_exists) + .and_then(commit) + .value(); +} + +} // namespace test_foreign_key + +#endif diff --git a/tests/sqlite/test_foreign_key.cpp b/tests/sqlite/test_foreign_key.cpp new file mode 100644 index 0000000..b025731 --- /dev/null +++ b/tests/sqlite/test_foreign_key.cpp @@ -0,0 +1,58 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_foreign_key { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +struct Relationship { + sqlgen::ForeignKey parent_id; + uint32_t child_id; +}; + +TEST(sqlite, test_foreign_key) { + auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto relationships = + std::vector({Relationship{.parent_id = 0, .child_id = 2}, + Relationship{.parent_id = 0, .child_id = 3}, + Relationship{.parent_id = 0, .child_id = 4}, + Relationship{.parent_id = 1, .child_id = 2}, + Relationship{.parent_id = 1, .child_id = 3}, + Relationship{.parent_id = 1, .child_id = 4}}); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto people = sqlite::connect() + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(begin_transaction) + .and_then(create_table) + .and_then(create_table) + .and_then(insert(std::ref(people1))) + .and_then(insert(std::ref(relationships))) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(commit) + .value(); +} + +} // namespace test_foreign_key +