diff --git a/docs/README.md b/docs/README.md index e1e079ac..b5a5624b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::inner_join, sqlgen::left_join, sqlgen::right_join, sqlgen::full_join](joins.md) - How to join different tables - [sqlgen::insert](insert.md) - How to insert data within transactions - [sqlgen::select_from](select_from.md) - How to read data from a database using more complex queries +- [sqlgen::unite and sqlgen::unite_all](unite.md) - How to combine results from multiple SELECT statements - [sqlgen::update](update.md) - How to update data in a table ## Other Operations diff --git a/docs/unite.md b/docs/unite.md new file mode 100644 index 00000000..f64273af --- /dev/null +++ b/docs/unite.md @@ -0,0 +1,82 @@ +# `unite` and `unite_all` + +The `unite` and `unite_all` functions allow you to combine the results of multiple `SELECT` statements into a single result set. + +## `unite` + +The `unite` function corresponds to the SQL `UNION` operator. It combines the result sets of two or more `SELECT` statements and removes duplicate rows. + +### Example + +```cpp +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +const auto s1 = sqlgen::select_from("name"_c, "age"_c); +const auto s2 = sqlgen::select_from("name"_c, "age"_c); + +const auto united = sqlgen::unite>(s1, s2); +``` + +## `unite_all` + +The `unite_all` function corresponds to the SQL `UNION ALL` operator. It combines the result sets of two or more `SELECT` statements, including all duplicate rows. + +### Example + +```cpp +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +const auto s1 = sqlgen::select_from("name"_c, "age"_c); +const auto s2 = sqlgen::select_from("name"_c, "age"_c); + +const auto united = sqlgen::unite_all>(s1, s2); +``` + +## Nesting in `SELECT` statements + +You can use the result of a `unite` or `unite_all` operation as a subquery in a `SELECT` statement. + +### Example + +```cpp +const auto united = sqlgen::unite>(s1, s2); + +const auto sel = sqlgen::select_from(united.as("u"), "name"_c, "age"_c); +``` + +## Nesting in `JOIN` statements + +You can also use the result of a `unite` or `unite_all` operation as a subquery in a `JOIN` statement. + +### Example + +```cpp +struct Login { + int id; + std::string username; +}; + +const auto united = sqlgen::unite>(s1, s2); + +const auto sel = + sqlgen::select_from( + "id"_c, "username"_c, + sqlgen::inner_join(united.as("u"), "username"_c == "u.name"_c)) + .where("id"_c == 1); +``` diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index c9faeeba..d79a4f5d 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -45,6 +45,7 @@ #include "sqlgen/select_from.hpp" #include "sqlgen/sqlgen_api.hpp" #include "sqlgen/to.hpp" +#include "sqlgen/unite.hpp" #include "sqlgen/update.hpp" #include "sqlgen/where.hpp" #include "sqlgen/write.hpp" diff --git a/include/sqlgen/dynamic/SelectFrom.hpp b/include/sqlgen/dynamic/SelectFrom.hpp index c0ee22a0..31d3e742 100644 --- a/include/sqlgen/dynamic/SelectFrom.hpp +++ b/include/sqlgen/dynamic/SelectFrom.hpp @@ -18,7 +18,13 @@ namespace sqlgen::dynamic { struct SelectFrom { - using TableOrQueryType = rfl::Variant>; + struct Union { + std::vector columns; + Ref> selects; + bool all = false; + }; + + using TableOrQueryType = rfl::Variant, Ref>; struct Field { Operation val; diff --git a/include/sqlgen/dynamic/Statement.hpp b/include/sqlgen/dynamic/Statement.hpp index 63226e05..63b97f09 100644 --- a/include/sqlgen/dynamic/Statement.hpp +++ b/include/sqlgen/dynamic/Statement.hpp @@ -10,6 +10,7 @@ #include "Drop.hpp" #include "Insert.hpp" #include "SelectFrom.hpp" +#include "Union.hpp" #include "Update.hpp" #include "Write.hpp" @@ -17,7 +18,7 @@ namespace sqlgen::dynamic { using Statement = rfl::TaggedUnion<"stmt", CreateAs, CreateIndex, CreateTable, DeleteFrom, - Drop, Insert, SelectFrom, Update, Write>; + Drop, Insert, SelectFrom, Union, Update, Write>; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/dynamic/Union.hpp b/include/sqlgen/dynamic/Union.hpp new file mode 100644 index 00000000..47199a9c --- /dev/null +++ b/include/sqlgen/dynamic/Union.hpp @@ -0,0 +1,12 @@ +#ifndef SQLGEN_DYNAMIC_UNION_HPP_ +#define SQLGEN_DYNAMIC_UNION_HPP_ + +#include "SelectFrom.hpp" + +namespace sqlgen::dynamic { + +using Union = SelectFrom::Union; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/internal/GetColType.hpp b/include/sqlgen/internal/GetColType.hpp index bd4ea4ac..da52ff8b 100644 --- a/include/sqlgen/internal/GetColType.hpp +++ b/include/sqlgen/internal/GetColType.hpp @@ -17,12 +17,18 @@ struct GetColType { static Type get_value(const T& _t) { return _t; } }; -template -struct GetColType> { - using Type = transpilation::Col<_name>; - static Type get_value(const auto&) { return transpilation::Col<_name>{}; } +template +struct GetColType> { + using Type = transpilation::Col<_name, _alias>; + static Type get_value(const auto&) { + return transpilation::Col<_name, _alias>{}; + } }; +template +using get_col_type_t = typename GetColType::Type; + } // namespace sqlgen::internal #endif diff --git a/include/sqlgen/internal/all_same_v.hpp b/include/sqlgen/internal/all_same_v.hpp new file mode 100644 index 00000000..4ac2e8e6 --- /dev/null +++ b/include/sqlgen/internal/all_same_v.hpp @@ -0,0 +1,23 @@ +#include +#include +#include + +namespace sqlgen::internal { + +template +struct AllSame; + +template +struct AllSame> { + static constexpr bool value = std::conjunction_v...>; +}; + +template +struct AllSame> { + static constexpr bool value = std::conjunction_v...>; +}; + +template +constexpr bool all_same_v = AllSame>::value; + +} // namespace sqlgen::internal diff --git a/include/sqlgen/select_from.hpp b/include/sqlgen/select_from.hpp index 4786afbb..47fee30d 100644 --- a/include/sqlgen/select_from.hpp +++ b/include/sqlgen/select_from.hpp @@ -96,11 +96,21 @@ auto select_from_impl(const Result>& _res, }); } -template +template struct SelectFrom { + using TableOrQueryType = _TableOrQueryType; + using AliasType = _AliasType; + using FieldsType = _FieldsType; + using JoinsType = _JoinsType; + using WhereType = _WhereType; + using GroupByType = _GroupByType; + using OrderByType = _OrderByType; + using LimitType = _LimitType; + using ToType = _ToType; + auto operator()(const auto& _conn) const { using TableTupleType = transpilation::table_tuple_t; @@ -347,7 +357,7 @@ inline auto select_from(const FieldTypes&... _fields) { .from_ = transpilation::TableWrapper{}}; } -template inline auto select_from(const QueryType& _query, const FieldTypes&... _fields) { using FieldsType = diff --git a/include/sqlgen/sqlite/Connection.hpp b/include/sqlgen/sqlite/Connection.hpp index bc344049..ec04dd08 100644 --- a/include/sqlgen/sqlite/Connection.hpp +++ b/include/sqlgen/sqlite/Connection.hpp @@ -13,6 +13,8 @@ #include "../Ref.hpp" #include "../Result.hpp" #include "../Transaction.hpp" +#include "../dynamic/SelectFrom.hpp" +#include "../dynamic/Union.hpp" #include "../dynamic/Write.hpp" #include "../internal/to_container.hpp" #include "../internal/write_or_insert.hpp" @@ -50,7 +52,7 @@ class SQLGEN_API Connection { } template - auto read(const dynamic::SelectFrom& _query) { + auto read(const rfl::Variant& _query) { using ValueType = transpilation::value_t; return internal::to_container( read_impl(_query).transform([](auto&& _it) { @@ -92,7 +94,8 @@ class SQLGEN_API Connection { Result prepare_statement(const std::string& _sql) const noexcept; /// Implements the actual read. - Result> read_impl(const dynamic::SelectFrom& _query); + Result> read_impl( + const rfl::Variant& _query); /// Implements the actual write Result write_impl( diff --git a/include/sqlgen/transpilation/Union.hpp b/include/sqlgen/transpilation/Union.hpp new file mode 100644 index 00000000..20ddddd5 --- /dev/null +++ b/include/sqlgen/transpilation/Union.hpp @@ -0,0 +1,14 @@ +#ifndef SQLGEN_TRANSPILATION_UNION_HPP_ +#define SQLGEN_TRANSPILATION_UNION_HPP_ + +#include +#include + +namespace sqlgen::transpilation { + +template +struct Union {}; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/fields_to_named_tuple_t.hpp b/include/sqlgen/transpilation/fields_to_named_tuple_t.hpp index 8b2f9ff4..88feb2c4 100644 --- a/include/sqlgen/transpilation/fields_to_named_tuple_t.hpp +++ b/include/sqlgen/transpilation/fields_to_named_tuple_t.hpp @@ -4,7 +4,10 @@ #include #include "../Literal.hpp" +#include "../internal/all_same_v.hpp" +#include "Union.hpp" #include "make_field.hpp" +#include "table_tuple_t.hpp" namespace sqlgen::transpilation { @@ -38,6 +41,32 @@ struct FieldsToNamedTupleType> { using Type = typename FieldsToNamedTupleType::Type; }; +template +struct FieldsToNamedTupleType> { + using NamedTupleTypes = rfl::Tuple, + typename SelectTs::FieldsType>::Type...>; + static_assert( + sqlgen::internal::all_same_v, + "All SELECT statements in a UNION must return the same columns with " + "the same types."); + using Type = rfl::tuple_element_t<0, NamedTupleTypes>; +}; + +template +struct FieldsToNamedTupleType, FieldTypes...> { + using Type = typename FieldsToNamedTupleType< + typename FieldsToNamedTupleType>::Type, + FieldTypes...>::Type; +}; + +template +struct FieldsToNamedTupleType, rfl::Tuple> { + using Type = + typename FieldsToNamedTupleType, FieldTypes...>::Type; +}; + template using fields_to_named_tuple_t = typename FieldsToNamedTupleType::Type; diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index 05f79fc0..88b5279d 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -12,6 +12,7 @@ #include "../insert.hpp" #include "../read.hpp" #include "../select_from.hpp" +#include "../unite.hpp" #include "../update.hpp" #include "columns_t.hpp" #include "read_to_select_from.hpp" @@ -22,6 +23,7 @@ #include "to_drop.hpp" #include "to_insert_or_write.hpp" #include "to_select_from.hpp" +#include "to_union.hpp" #include "to_update.hpp" #include "value_t.hpp" @@ -120,6 +122,13 @@ struct ToSQL> { } }; +template +struct ToSQL> { + dynamic::Statement operator()(const auto& _union) const { + return to_union(_union.selects_); + } +}; + template dynamic::Statement to_sql(const T& _t) { return ToSQL>{}(_t); diff --git a/include/sqlgen/transpilation/to_union.hpp b/include/sqlgen/transpilation/to_union.hpp new file mode 100644 index 00000000..2b88652a --- /dev/null +++ b/include/sqlgen/transpilation/to_union.hpp @@ -0,0 +1,45 @@ +#ifndef SQLGEN_TRANSPILATION_TO_UNION_HPP_ +#define SQLGEN_TRANSPILATION_TO_UNION_HPP_ + +#include +#include +#include + +#include "../Ref.hpp" +#include "../dynamic/SelectFrom.hpp" +#include "../dynamic/Union.hpp" +#include "table_tuple_t.hpp" +#include "to_select_from.hpp" +#include "value_t.hpp" + +namespace sqlgen::transpilation { + +template +dynamic::Union to_union(const rfl::Tuple& _selects) noexcept { + using ValueType = value_t; + using NamedTupleType = rfl::named_tuple_t; + + const auto columns = NamedTupleType::Names::names(); + + const auto selects = rfl::apply( + [](const auto... _s) { + return Ref>::make( + std::vector({to_select_from< + table_tuple_t, + typename SelectTs::AliasType, typename SelectTs::FieldsType, + typename SelectTs::TableOrQueryType, + typename SelectTs::JoinsType, typename SelectTs::WhereType, + typename SelectTs::GroupByType, typename SelectTs::OrderByType, + typename SelectTs::LimitType>(_s.fields_, _s.from_, _s.joins_, + _s.where_, _s.limit_)...})); + }, + _selects); + + return dynamic::Union{.columns = columns, .selects = selects}; +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/unite.hpp b/include/sqlgen/unite.hpp new file mode 100644 index 00000000..9f8a94d7 --- /dev/null +++ b/include/sqlgen/unite.hpp @@ -0,0 +1,168 @@ +#ifndef SQLGEN_UNITE_HPP_ +#define SQLGEN_UNITE_HPP_ + +#include +#include + +#include "Literal.hpp" +#include "Range.hpp" +#include "Ref.hpp" +#include "Result.hpp" +#include "dynamic/Union.hpp" +#include "internal/is_range.hpp" +#include "internal/iterator_t.hpp" +#include "is_connection.hpp" +#include "transpilation/Union.hpp" +#include "transpilation/fields_to_named_tuple_t.hpp" +#include "transpilation/get_table_t.hpp" +#include "transpilation/to_union.hpp" +#include "transpilation/value_t.hpp" +#include "transpilation/wrap_in_optional_t.hpp" + +namespace sqlgen { + +template + requires is_connection +auto unite_impl(const Ref& _conn, + const rfl::Tuple& _selects, const bool _all) { + if constexpr (internal::is_range_v) { + auto query = transpilation::to_union(_selects); + query.all = _all; + return _conn->template read(query); + + } else { + const auto to_container = [](auto range) -> Result { + using ValueType = transpilation::value_t; + ContainerType container; + for (auto& res : range) { + if (res) { + container.emplace_back( + rfl::from_named_tuple(std::move(*res))); + } else { + return error("One of the results in the union was an error."); + } + } + return container; + }; + + using IteratorType = + internal::iterator_t>, + decltype(_conn)>; + + using RangeType = Range; + + return unite_impl(_conn, _selects, _all).and_then(to_container); + } +} + +template +struct Union { + template + requires is_connection + auto operator()(const Ref& _conn) const { + using ContainerType = std::conditional_t< + std::is_same_v, Nothing>, + Range>, + Connection>>, + _ContainerType>; + + return unite_impl(_conn, selects_, all_); + } + + template + requires is_connection + auto operator()(const Result>& _res) const { + return _res.and_then([&](const auto& _conn) { return (*this)(_conn); }); + } + + rfl::Tuple selects_; + bool all_ = false; +}; + +namespace transpilation { + +template +struct ExtractTable, false> { + using Type = std::conditional_t< + std::is_same_v, Nothing>, + fields_to_named_tuple_t>, value_t>; +}; + +template +struct ExtractTable, true> { + using Type = wrap_in_optional_t, false>::Type>; +}; + +template +struct ToTableOrQuery> { + dynamic::SelectFrom::TableOrQueryType operator()(const auto& _query) { + return Ref::make(to_union(_query.selects_)); + } +}; + +template +struct FieldsToNamedTupleType, + FieldTs...> { + using Type = fields_to_named_tuple_t, FieldTs...>; +}; + +template +struct FieldsToNamedTupleType, + rfl::Tuple> { + using Type = fields_to_named_tuple_t, FieldTs...>; +}; + +template +struct GetTableType, + sqlgen::Union> { + using TableType = get_table_t< + Literal<_alias>, + extract_table_t, false>>; +}; + +template +struct GetTableType, sqlgen::Union> { + using TableType = get_table_t< + Literal<"">, + extract_table_t, false>>; +}; + +template +struct GetTableType, + sqlgen::Union> { + using TableType = get_table_t< + std::integral_constant, + extract_table_t, false>>; +}; + +} // namespace transpilation + +template +auto unite(const SelectTs&... _selects) { + return Union{ + .selects_ = rfl::Tuple(_selects...), .all_ = false}; +} + +template +auto unite(const SelectTs&... _selects) { + return unite(_selects...); +} + +template +auto unite_all(const SelectTs&... _selects) { + return Union{ + .selects_ = rfl::Tuple(_selects...), .all_ = true}; +} + +template +auto unite_all(const SelectTs&... _selects) { + return unite_all(_selects...); +} + +} // namespace sqlgen + +#endif diff --git a/src/sqlgen/duckdb/to_sql.cpp b/src/sqlgen/duckdb/to_sql.cpp index 498e913b..8f51d018 100644 --- a/src/sqlgen/duckdb/to_sql.cpp +++ b/src/sqlgen/duckdb/to_sql.cpp @@ -72,6 +72,8 @@ std::string table_or_query_to_sql( std::string type_to_sql(const dynamic::Type& _type) noexcept; +std::string union_to_sql(const dynamic::Union& _stmt) noexcept; + std::string update_to_sql(const dynamic::Update& _stmt) noexcept; std::string write_to_sql(const dynamic::Write& _stmt) noexcept; @@ -798,6 +800,10 @@ std::string table_or_query_to_sql( return wrap_in_quotes(*_t.schema) + "." + wrap_in_quotes(_t.name); } return wrap_in_quotes(_t.name); + + } else if constexpr (std::is_same_v>) { + return "(" + union_to_sql(*_t) + ")"; + } else { return "(" + select_from_to_sql(*_t) + ")"; } @@ -829,6 +835,9 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { } else if constexpr (std::is_same_v) { return select_from_to_sql(_s); + } else if constexpr (std::is_same_v) { + return union_to_sql(_s); + } else if constexpr (std::is_same_v) { return update_to_sql(_s); @@ -909,6 +918,26 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept { }); } +std::string union_to_sql(const dynamic::Union& _stmt) noexcept { + using namespace std::ranges::views; + + const auto columns = internal::strings::join( + ", ", + internal::collect::vector(_stmt.columns | transform(wrap_in_quotes))); + + const auto to_str = [&](const auto& _select) { + return "SELECT " + columns + " FROM (" + select_from_to_sql(_select) + ")"; + }; + + const auto separator = + _stmt.all ? std::string(" UNION ALL ") : std::string(" UNION "); + + return internal::strings::join( + separator, + internal::collect::vector(*_stmt.selects | transform(to_str))) + + ";"; +} + std::string update_to_sql(const dynamic::Update& _stmt) noexcept { using namespace std::ranges::views; diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index 35ef6592..17a07eb3 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -72,6 +72,8 @@ std::string table_or_query_to_sql( std::string type_to_sql(const dynamic::Type& _type) noexcept; +std::string union_to_sql(const dynamic::Union& _stmt) noexcept; + std::string update_to_sql(const dynamic::Update& _stmt) noexcept; // ---------------------------------------------------------------------------- @@ -837,6 +839,10 @@ std::string table_or_query_to_sql( return wrap_in_quotes(*_t.schema) + "." + wrap_in_quotes(_t.name); } return wrap_in_quotes(_t.name); + + } else if constexpr (std::is_same_v>) { + return "(" + union_to_sql(*_t) + ")"; + } else { return "(" + select_from_to_sql(*_t) + ")"; } @@ -872,12 +878,35 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { } else if constexpr (std::is_same_v) { return update_to_sql(_s); + } else if constexpr (std::is_same_v) { + return union_to_sql(_s); + } else { static_assert(rfl::always_false_v, "Unsupported type."); } }); } +std::string union_to_sql(const dynamic::Union& _stmt) noexcept { + using namespace std::ranges::views; + + const auto columns = internal::strings::join( + ", ", + internal::collect::vector(_stmt.columns | transform(wrap_in_quotes))); + + const auto to_str = [&](const auto& _select) { + return "SELECT " + columns + " FROM (" + select_from_to_sql(_select) + ")"; + }; + + const auto separator = + _stmt.all ? std::string(" UNION ALL ") : std::string(" UNION "); + + return internal::strings::join( + separator, + internal::collect::vector(*_stmt.selects | transform(to_str))) + + ";"; +} + std::string type_to_sql(const dynamic::Type& _type) noexcept { return _type.visit([](const auto _t) -> std::string { using T = std::remove_cvref_t; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index ab491aca..32e0e023 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -61,6 +61,8 @@ std::string table_or_query_to_sql( std::string type_to_sql(const dynamic::Type& _type) noexcept; +std::string union_to_sql(const dynamic::Union& _stmt) noexcept; + std::string update_to_sql(const dynamic::Update& _stmt) noexcept; std::string write_to_sql(const dynamic::Write& _stmt) noexcept; @@ -762,6 +764,10 @@ std::string table_or_query_to_sql( return wrap_in_quotes(*_t.schema) + "." + wrap_in_quotes(_t.name); } return wrap_in_quotes(_t.name); + + } else if constexpr (std::is_same_v>) { + return "(" + union_to_sql(*_t) + ")"; + } else { return "(" + select_from_to_sql(*_t) + ")"; } @@ -799,12 +805,35 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { } else if constexpr (std::is_same_v) { return write_to_sql(_s); + } else if constexpr (std::is_same_v) { + return union_to_sql(_s); + } else { static_assert(rfl::always_false_v, "Unsupported type."); } }); } +std::string union_to_sql(const dynamic::Union& _stmt) noexcept { + using namespace std::ranges::views; + + const auto columns = internal::strings::join( + ", ", + internal::collect::vector(_stmt.columns | transform(wrap_in_quotes))); + + const auto to_str = [&](const auto& _select) { + return "SELECT " + columns + " FROM (" + select_from_to_sql(_select) + ")"; + }; + + const auto separator = + _stmt.all ? std::string(" UNION ALL ") : std::string(" UNION "); + + return internal::strings::join( + separator, + internal::collect::vector(*_stmt.selects | transform(to_str))) + + ";"; +} + std::string type_to_sql(const dynamic::Type& _type) noexcept { return _type.visit([](const auto _t) -> std::string { using T = std::remove_cvref_t; diff --git a/src/sqlgen/sqlite/Connection.cpp b/src/sqlgen/sqlite/Connection.cpp index 0aa44d98..3d60ccbc 100644 --- a/src/sqlgen/sqlite/Connection.cpp +++ b/src/sqlgen/sqlite/Connection.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "sqlgen/internal/collect/vector.hpp" @@ -101,8 +102,9 @@ typename Connection::ConnPtr Connection::make_conn(const std::string& _fname) { return ConnPtr::make(std::shared_ptr(conn, &sqlite3_close)).value(); } -Result> Connection::read_impl(const dynamic::SelectFrom& _query) { - const auto sql = to_sql_impl(_query); +Result> Connection::read_impl( + const rfl::Variant& _query) { + const auto sql = _query.visit([](const auto& _q) { return to_sql_impl(_q); }); sqlite3_stmt* p_stmt = nullptr; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index d38e6873..11551c72 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -53,6 +53,8 @@ std::string table_or_query_to_sql( std::string type_to_sql(const dynamic::Type& _type) noexcept; +std::string union_to_sql(const dynamic::Union& _stmt) noexcept; + std::string update_to_sql(const dynamic::Update& _stmt) noexcept; // ---------------------------------------------------------------------------- @@ -720,6 +722,10 @@ std::string table_or_query_to_sql( return wrap_in_quotes(*_t.schema) + "." + wrap_in_quotes(_t.name); } return wrap_in_quotes(_t.name); + + } else if constexpr (std::is_same_v>) { + return "(" + union_to_sql(*_t) + ")"; + } else { return "(" + select_from_to_sql(*_t) + ")"; } @@ -756,12 +762,33 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { } else if constexpr (std::is_same_v) { return insert_or_write_to_sql(_s); + } else if constexpr (std::is_same_v) { + return union_to_sql(_s); + } else { static_assert(rfl::always_false_v, "Unsupported type."); } }); } +std::string union_to_sql(const dynamic::Union& _stmt) noexcept { + using namespace std::ranges::views; + + const auto columns = internal::strings::join( + ", ", + internal::collect::vector(_stmt.columns | transform(wrap_in_quotes))); + + const auto to_str = [&](const auto& _select) { + return "SELECT " + columns + " FROM (" + select_from_to_sql(_select) + ")"; + }; + + const auto separator = + _stmt.all ? std::string(" UNION ALL ") : std::string(" UNION "); + + return internal::strings::join( + separator, internal::collect::vector(*_stmt.selects | transform(to_str))); +} + std::string type_to_sql(const dynamic::Type& _type) noexcept { return _type.visit([](const auto _t) -> std::string { using T = std::remove_cvref_t; diff --git a/tests/duckdb/unite.cpp b/tests/duckdb/unite.cpp new file mode 100644 index 00000000..f6eea04e --- /dev/null +++ b/tests/duckdb/unite.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/duckdb/to_sql.hpp" + +namespace test_unite { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +struct Login { + int id; + std::string username; +}; + +TEST(duckdb, test_union) { + using namespace sqlgen::literals; + + // Connect to duckdb database + const auto conn = sqlgen::duckdb::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::duckdb::to_sql(sqlgen::unite>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(duckdb, test_union_all) { + using namespace sqlgen::literals; + + // Connect to duckdb database + const auto conn = sqlgen::duckdb::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 30, .name = "John"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite_all>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::duckdb::to_sql(sqlgen::unite_all>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION ALL SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION ALL SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "John"); + EXPECT_EQ(users.at(0).age, 30); + EXPECT_EQ(users.at(1).name, "Jane"); + EXPECT_EQ(users.at(1).age, 25); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(duckdb, test_union_in_select) { + using namespace sqlgen::literals; + + // Connect to duckdb database + const auto conn = sqlgen::duckdb::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = sqlgen::select_from(united.as("u"), "name"_c, "age"_c); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::duckdb::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");) u)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(duckdb, test_union_in_join) { + using namespace sqlgen::literals; + + // Connect to duckdb database + const auto conn = sqlgen::duckdb::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto login = Login{.id = 1, .username = "John"}; + sqlgen::write(conn, login); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = + sqlgen::select_from( + "id"_c, "username"_c, + sqlgen::inner_join(united.as("u"), "username"_c == "u.name"_c)) + .where("id"_c == 1); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::duckdb::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT "id", "username" FROM "Login" INNER JOIN (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");) u ON "username" = u."name" WHERE "id" = 1)"); + + EXPECT_EQ(users.size(), 1); + EXPECT_EQ(users.at(0).id, 1); + EXPECT_EQ(users.at(0).username, "John"); +} + +} // namespace test_unite diff --git a/tests/mysql/unite.cpp b/tests/mysql/unite.cpp new file mode 100644 index 00000000..1525f621 --- /dev/null +++ b/tests/mysql/unite.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/mysql/to_sql.hpp" + +namespace test_unite { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +struct Login { + int id; + std::string username; +}; + +TEST(mysql, test_union) { + using namespace sqlgen::literals; + + // Connect to mysql database + const auto conn = sqlgen::mysql::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::mysql::to_sql(sqlgen::unite>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User1`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User2`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User3`))"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(mysql, test_union_all) { + using namespace sqlgen::literals; + + // Connect to mysql database + const auto conn = sqlgen::mysql::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 30, .name = "John"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite_all>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::mysql::to_sql(sqlgen::unite_all>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User1`) UNION ALL SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User2`) UNION ALL SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User3`))"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "John"); + EXPECT_EQ(users.at(0).age, 30); + EXPECT_EQ(users.at(1).name, "Jane"); + EXPECT_EQ(users.at(1).age, 25); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(mysql, test_union_in_select) { + using namespace sqlgen::literals; + + // Connect to mysql database + const auto conn = sqlgen::mysql::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = sqlgen::select_from(united.as("u"), "name"_c, "age"_c); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::mysql::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT `name`, `age` FROM (SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User1`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User2`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User3`)) u)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(mysql, test_union_in_join) { + using namespace sqlgen::literals; + + // Connect to mysql database + const auto conn = sqlgen::mysql::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto login = Login{.id = 1, .username = "John"}; + sqlgen::write(conn, login); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = + sqlgen::select_from( + "id"_c, "username"_c, + sqlgen::inner_join(united.as("u"), "username"_c == "u.name"_c)) + .where("id"_c == 1); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::mysql::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT `id`, `username` FROM `Login` INNER JOIN (SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User1`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User2`) UNION SELECT `name`, `age` FROM (SELECT `name`, `age` FROM `User3`)) u ON `username` = u.`name` WHERE `id` = 1)"); + + EXPECT_EQ(users.size(), 1); + EXPECT_EQ(users.at(0).id, 1); + EXPECT_EQ(users.at(0).username, "John"); +} + +} // namespace test_unite diff --git a/tests/postgres/unite.cpp b/tests/postgres/unite.cpp new file mode 100644 index 00000000..462b3f2a --- /dev/null +++ b/tests/postgres/unite.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/postgres/to_sql.hpp" + +namespace test_unite { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +struct Login { + int id; + std::string username; +}; + +TEST(postgres, test_union) { + using namespace sqlgen::literals; + + // Connect to postgres database + const auto conn = sqlgen::postgres::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::postgres::to_sql(sqlgen::unite>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(postgres, test_union_all) { + using namespace sqlgen::literals; + + // Connect to postgres database + const auto conn = sqlgen::postgres::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 30, .name = "John"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite_all>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::postgres::to_sql(sqlgen::unite_all>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION ALL SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION ALL SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "John"); + EXPECT_EQ(users.at(0).age, 30); + EXPECT_EQ(users.at(1).name, "Jane"); + EXPECT_EQ(users.at(1).age, 25); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(postgres, test_union_in_select) { + using namespace sqlgen::literals; + + // Connect to postgres database + const auto conn = sqlgen::postgres::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = sqlgen::select_from(united.as("u"), "name"_c, "age"_c); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::postgres::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");) u)"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +TEST(postgres, test_union_in_join) { + using namespace sqlgen::literals; + + // Connect to postgres database + const auto conn = sqlgen::postgres::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto login = Login{.id = 1, .username = "John"}; + sqlgen::write(conn, login); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = + sqlgen::select_from( + "id"_c, "username"_c, + sqlgen::inner_join(united.as("u"), "username"_c == "u.name"_c)) + .where("id"_c == 1); + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::postgres::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT "id", "username" FROM "Login" INNER JOIN (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3");) u ON "username" = u."name" WHERE "id" = 1)"); + + EXPECT_EQ(users.size(), 1); + EXPECT_EQ(users.at(0).id, 1); + EXPECT_EQ(users.at(0).username, "John"); +} + +} // namespace test_unite diff --git a/tests/sqlite/test_union.cpp b/tests/sqlite/test_union.cpp new file mode 100644 index 00000000..03a3a2f4 --- /dev/null +++ b/tests/sqlite/test_union.cpp @@ -0,0 +1,66 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/sqlite/to_sql.hpp" + +namespace test_union { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +TEST(sqlite, test_union) { + using namespace sqlgen::literals; + + const auto conn = sqlgen::sqlite::connect(); + + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::sqlite::to_sql(sqlgen::unite>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3"))"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +} // namespace test_union diff --git a/tests/sqlite/test_union_all.cpp b/tests/sqlite/test_union_all.cpp new file mode 100644 index 00000000..c9dd191e --- /dev/null +++ b/tests/sqlite/test_union_all.cpp @@ -0,0 +1,66 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/sqlite/to_sql.hpp" + +namespace test_union_all { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +TEST(sqlite, test_union_all) { + using namespace sqlgen::literals; + + const auto conn = sqlgen::sqlite::connect(); + + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 30, .name = "John"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto result = sqlgen::unite_all>(s1, s2, s3)(conn); + + const auto users = result.value(); + + const auto query = + sqlgen::sqlite::to_sql(sqlgen::unite_all>(s1, s2, s3)); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3"))"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "John"); + EXPECT_EQ(users.at(0).age, 30); + EXPECT_EQ(users.at(1).name, "Jane"); + EXPECT_EQ(users.at(1).age, 25); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +} // namespace test_union_all diff --git a/tests/sqlite/test_union_in_join.cpp b/tests/sqlite/test_union_in_join.cpp new file mode 100644 index 00000000..b4a9e778 --- /dev/null +++ b/tests/sqlite/test_union_in_join.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/sqlite/to_sql.hpp" + +namespace test_union_in_join { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +struct Login { + int id; + std::string username; +}; + +TEST(sqlite, test_union_in_join) { + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = sqlgen::sqlite::connect(); + + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto login = Login{.id = 1, .username = "John"}; + sqlgen::write(conn, login); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = select_from("id"_t1, "username"_t1) | + inner_join<"t2">(united, "username"_t1 == "name"_t2) | + where("id"_t1 == 1) | to>; + + const auto query = sqlgen::sqlite::to_sql(sel); + + const auto result = sel(conn); + + const auto users = result.value(); + + EXPECT_EQ( + query, + R"(SELECT t1."id", t1."username" FROM "Login" t1 INNER JOIN (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3")) t2 ON t1."username" = t2."name" WHERE t1."id" = 1)"); + + EXPECT_EQ(users.size(), 1); + EXPECT_EQ(users.at(0).id, 1); + EXPECT_EQ(users.at(0).username, "John"); +} + +} // namespace test_union_in_join diff --git a/tests/sqlite/test_union_in_select.cpp b/tests/sqlite/test_union_in_select.cpp new file mode 100644 index 00000000..2b08b4b6 --- /dev/null +++ b/tests/sqlite/test_union_in_select.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include +#include +#include +#include + +#include "sqlgen/sqlite/to_sql.hpp" + +namespace test_union_in_select { + +struct User1 { + std::string name; + int age; +}; + +struct User2 { + std::string name; + int age; +}; + +struct User3 { + int age; + std::string name; +}; + +TEST(sqlite, test_union_in_select) { + using namespace sqlgen::literals; + + // Connect to SQLite database + const auto conn = sqlgen::sqlite::connect(); + + // Create and insert a user + const auto user1 = User1{.name = "John", .age = 30}; + sqlgen::write(conn, user1); + + const auto user2 = User2{.name = "Jane", .age = 25}; + sqlgen::write(conn, user2); + + const auto user3 = User3{.age = 40, .name = "Joe"}; + sqlgen::write(conn, user3); + + const auto s1 = sqlgen::select_from("name"_c, "age"_c); + const auto s2 = sqlgen::select_from("name"_c, "age"_c); + const auto s3 = sqlgen::select_from("name"_c, "age"_c); + + const auto united = sqlgen::unite>(s1, s2, s3); + + const auto sel = sqlgen::select_from(united, "name"_c, "age"_c) | + sqlgen::to>; + + const auto result = sel(conn); + + const auto users = result.value(); + + const auto query = sqlgen::sqlite::to_sql(sel); + + EXPECT_EQ( + query, + R"(SELECT "name", "age" FROM (SELECT "name", "age" FROM (SELECT "name", "age" FROM "User1") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User2") UNION SELECT "name", "age" FROM (SELECT "name", "age" FROM "User3")))"); + + EXPECT_EQ(users.size(), 3); + EXPECT_EQ(users.at(0).name, "Jane"); + EXPECT_EQ(users.at(0).age, 25); + EXPECT_EQ(users.at(1).name, "Joe"); + EXPECT_EQ(users.at(1).age, 40); + EXPECT_EQ(users.at(2).name, "John"); + EXPECT_EQ(users.at(2).age, 30); +} + +} // namespace test_union_in_select