diff --git a/docs/README.md b/docs/README.md index 3f99bab..fa03d0e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::exec](exec.md) - How to execute raw SQL statements - [sqlgen::group_by and Aggregations](group_by_and_aggregations.md) - How generate GROUP BY queries and aggregate data - [sqlgen::insert](insert.md) - How to insert data within transactions +- [Joins](joins.md) - How to join different tables - [sqlgen::update](update.md) - How to update data in a table ## Other Operations diff --git a/docs/joins.md b/docs/joins.md new file mode 100644 index 0000000..70482d7 --- /dev/null +++ b/docs/joins.md @@ -0,0 +1,199 @@ +# `sqlgen::inner_join`, `sqlgen::left_join`, `sqlgen::right_join`, `sqlgen::full_join` + +The `sqlgen` library provides a type-safe, composable interface for expressing SQL joins in C++. It supports all standard SQL join types (inner, left, right, full) and allows for both simple and nested join queries, including joining on subqueries. + +## Join Types + +The following join types are available: + +- `inner_join` — Returns rows when there is a match in both tables +- `left_join` — Returns all rows from the left table, and matched rows from the right table +- `right_join` — Returns all rows from the right table, and matched rows from the left table +- `full_join` — Returns all rows when there is a match in one of the tables + +Each join type can be used with either a table or a subquery, and can be aliased for use in complex queries. + +## Usage + +### Basic Join Example + +```cpp +using namespace sqlgen; + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + sqlgen::PrimaryKey parent_id; + sqlgen::PrimaryKey child_id; +}; + +// Join two tables: Relationship (as t1) and Person (as t2) +const auto get_children = + select_from( + "parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age"> + ) | + left_join("id"_t2 == "child_id"_t1); +``` + +This produces a query equivalent to: + +```sql +SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" +FROM "Relationship" t1 +LEFT JOIN "Person" t2 ON t2."id" = t1."child_id" +``` + +### Chaining Multiple Joins + +You can chain multiple joins to build more complex queries: + +```cpp +const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t3 | as<"first_name_child">, + avg("age"_t1 - "age"_t3) | as<"avg_parent_age_at_birth"> + ) | + inner_join("id"_t1 == "parent_id"_t2) | + left_join("id"_t3 == "child_id"_t2) | + group_by("last_name"_t1, "first_name"_t3) | + order_by("last_name"_t1, "first_name"_t3) | + to>; +``` + +This produces a query with both an inner and a left join, grouping and ordering the results. + +### Nested Joins and Subqueries + +`sqlgen` allows you to join on subqueries, enabling nested join patterns: + +```cpp +const auto get_children = + select_from( + "parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age"> + ) | + left_join("id"_t2 == "child_id"_t1); + +const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t2 | as<"first_name_child">, + avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth"> + ) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + group_by("last_name"_t1, "first_name"_t2) | + order_by("first_name"_t2) | + to>; +``` + +This produces a query with a subquery in the join clause: + +```sql +SELECT t1."last_name" AS "last_name", t2."first_name" AS "first_name_child", AVG((t1."age") - (t2."age")) AS "avg_parent_age_at_birth" +FROM "Person" t1 +INNER JOIN ( + SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" + FROM "Relationship" t1 + LEFT JOIN "Person" t2 ON t2."id" = t1."child_id" +) t2 ON t1."id" = t2."id" +GROUP BY t1."last_name", t2."first_name" +ORDER BY t2."first_name" +``` + +## Join Syntax + +- `inner_join(condition)` — Inner join with a table +- `left_join(condition)` — Left join with a table +- `right_join(condition)` — Right join with a table +- `full_join(condition)` — Full join with a table +- `inner_join(subquery, condition)` — Inner join with a subquery +- `left_join(subquery, condition)` — Left join with a subquery +- `right_join(subquery, condition)` — Right join with a subquery +- `full_join(subquery, condition)` — Full join with a subquery + +Where: +- `Table` is the C++ struct representing the table +- `Alias` is a string literal alias for the table or subquery +- `condition` is a boolean expression relating columns from the joined tables +- `subquery` is a previously constructed query expression + +## Aliasing and Column References + +When joining tables or subqueries, you must use aliases to disambiguate columns. Use the `_t1`, `_t2`, etc. suffixes to refer to columns from different tables or subqueries: + +```cpp +"id"_t1, "first_name"_t2, "age"_t3 +``` + +The library includes predefined suffixes from `_t1` to `_t99`. + +If you want to use your own alias, you can use this syntax instead: + +```cpp +col<"id", "your_alias">, col<"first_name", "your_alias">, col<"age", "your_alias"> +``` + +## Advanced: Nested and Grouped Joins + +You can nest joins and combine them with grouping and aggregation: + +```cpp +const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t2 | as<"first_name_child">, + avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth"> + ) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + group_by("last_name"_t1, "first_name"_t2) | + order_by("first_name"_t2) | + to>; +``` + +## Notes + +- Joins can be chained and nested arbitrarily +- Aliases are required for all joined tables and subqueries +- Join conditions must use the correct aliases for columns +- You can join on subqueries, enabling powerful query composition +- Joins can be combined with `group_by`, `order_by`, and aggregation functions +- The result type must match the structure of your select statement, with field names matching the aliases used in the query + +## Example: Full Nested Join Query + +```cpp +struct ParentAndChild { + std::string last_name; + std::string first_name_child; + double avg_parent_age_at_birth; +}; + +const auto get_children = + select_from("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + left_join("id"_t2 == "child_id"_t1); + +const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t2 | as<"first_name_child">, + avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth"> + ) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + group_by("last_name"_t1, "first_name"_t2) | + order_by("first_name"_t2) | + to>; +``` + +This query joins a table to a subquery, groups by two columns, and computes an aggregate over the join. + diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index ed75e19..60f21cb 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -29,6 +29,7 @@ #include "sqlgen/if_not_exists.hpp" #include "sqlgen/insert.hpp" #include "sqlgen/is_connection.hpp" +#include "sqlgen/joins.hpp" #include "sqlgen/limit.hpp" #include "sqlgen/operations.hpp" #include "sqlgen/order_by.hpp" diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index e2ad471..67d63fd 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -19,20 +19,22 @@ namespace sqlgen { -template +template struct Col { - using ColType = transpilation::Col<_name>; + using ColType = transpilation::Col<_name, _alias>; using Name = rfl::Literal<_name>; + using Alias = rfl::Literal<_alias>; template auto as() const noexcept { - return transpilation::As, _new_name>{ - .val = transpilation::Col<_name>{}}; + return transpilation::As, _new_name>{ + .val = transpilation::Col<_name, _alias>{}}; } /// Signals to order_by that this column is to be sorted in descending order. auto desc() const noexcept { - return transpilation::Desc>{}; + return transpilation::Desc>{}; } /// Returns the column name. @@ -40,72 +42,76 @@ struct Col { /// Returns an IS NULL condition. auto is_null() const noexcept { - return transpilation::make_condition( - transpilation::conditions::is_null(transpilation::Col<_name>{})); + return transpilation::make_condition(transpilation::conditions::is_null( + transpilation::Col<_name, _alias>{})); } /// Returns a IS NOT NULL condition. auto is_not_null() const noexcept { - return transpilation::make_condition( - transpilation::conditions::is_not_null(transpilation::Col<_name>{})); + return transpilation::make_condition(transpilation::conditions::is_not_null( + transpilation::Col<_name, _alias>{})); } /// Returns a LIKE condition. auto like(const std::string& _pattern) const noexcept { - return transpilation::make_condition( - transpilation::conditions::like(transpilation::Col<_name>{}, _pattern)); + return transpilation::make_condition(transpilation::conditions::like( + transpilation::Col<_name, _alias>{}, _pattern)); } /// Returns a NOT LIKE condition. auto not_like(const std::string& _pattern) const noexcept { return transpilation::make_condition(transpilation::conditions::not_like( - transpilation::Col<_name>{}, _pattern)); + transpilation::Col<_name, _alias>{}, _pattern)); } /// Returns a SET clause in an UPDATE statement. template auto set(const T& _to) const noexcept { - return transpilation::Set, + return transpilation::Set, std::remove_cvref_t>{.to = _to}; } /// Returns a SET clause in an UPDATE statement. - template - auto set(const Col<_other_name>& _to) const noexcept { - return transpilation::Set, - transpilation::Col<_other_name>>{ - .to = transpilation::Col<_other_name>{}}; + template + auto set(const Col<_other_name, _other_alias>& _to) const noexcept { + return transpilation::Set, + transpilation::Col<_other_name, _other_alias>>{ + .to = transpilation::Col<_other_name, _other_alias>{}}; } /// Returns a SET clause in an UPDATE statement. auto set(const char* _to) const noexcept { - return transpilation::Set, std::string>{.to = - _to}; + return transpilation::Set, std::string>{ + .to = _to}; } template friend auto operator==(const Col&, const T& _t) { return transpilation::make_condition(transpilation::conditions::equal( - transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + transpilation::Col<_name, _alias>{}, + transpilation::to_transpilation_type(_t))); } template friend auto operator!=(const Col&, const T& _t) { return transpilation::make_condition(transpilation::conditions::not_equal( - transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + transpilation::Col<_name, _alias>{}, + transpilation::to_transpilation_type(_t))); } template friend auto operator<(const Col&, const T& _t) { return transpilation::make_condition(transpilation::conditions::lesser_than( - transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + transpilation::Col<_name, _alias>{}, + transpilation::to_transpilation_type(_t))); } template friend auto operator<=(const Col&, const T& _t) { return transpilation::make_condition( transpilation::conditions::lesser_equal( - transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>{}, transpilation::to_transpilation_type(_t))); } @@ -113,7 +119,7 @@ struct Col { friend auto operator>(const Col&, const T& _t) { return transpilation::make_condition( transpilation::conditions::greater_than( - transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>{}, transpilation::to_transpilation_type(_t))); } @@ -121,7 +127,7 @@ struct Col { friend auto operator>=(const Col&, const T& _t) { return transpilation::make_condition( transpilation::conditions::greater_equal( - transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>{}, transpilation::to_transpilation_type(_t))); } @@ -131,8 +137,9 @@ struct Col { std::remove_cvref_t>::Type; return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, + OtherType>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = transpilation::to_transpilation_type(_op2)}; } @@ -147,8 +154,9 @@ struct Col { std::remove_cvref_t>::Type; return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, + OtherType>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = transpilation::to_transpilation_type(_op2)}; } } @@ -159,8 +167,9 @@ struct Col { std::remove_cvref_t>::Type; return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, + OtherType>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = transpilation::to_transpilation_type(_op2)}; } @@ -170,8 +179,9 @@ struct Col { std::remove_cvref_t>::Type; return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, + OtherType>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = transpilation::to_transpilation_type(_op2)}; } @@ -181,8 +191,8 @@ struct Col { using DurationType = std::remove_cvref_t; return transpilation::Operation< transpilation::Operator::date_plus_duration, - transpilation::Col<_name>, rfl::Tuple>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, rfl::Tuple>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = rfl::Tuple(_op2)}; } else { @@ -190,33 +200,141 @@ struct Col { std::remove_cvref_t>::Type; return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, + transpilation::Col<_name, _alias>, + OtherType>{ + .operand1 = transpilation::Col<_name, _alias>{}, .operand2 = transpilation::to_transpilation_type(_op2)}; } } }; -template -const auto col = Col<_name>{}; +template +const auto col = Col<_name, _alias>{}; + +namespace transpilation { + +template +struct ToTranspilationType> { + using Type = transpilation::Col<_name, _alias>; + + Type operator()(const auto&) const noexcept { + return transpilation::Col<_name, _alias>{}; + } +}; +} // namespace transpilation template auto operator"" _c() { return Col<_name>{}; } -namespace transpilation { - -template -struct ToTranspilationType> { - using Type = transpilation::Col<_name>; - - Type operator()(const auto&) const noexcept { - return transpilation::Col<_name>{}; +#define SQLGEN_TABLE_ALIAS_LITERAL(N) \ + template \ + auto operator""_t##N() { \ + return Col<_name, "t" #N>{}; \ } -}; -} // namespace transpilation +SQLGEN_TABLE_ALIAS_LITERAL(1) +SQLGEN_TABLE_ALIAS_LITERAL(2) +SQLGEN_TABLE_ALIAS_LITERAL(3) +SQLGEN_TABLE_ALIAS_LITERAL(4) +SQLGEN_TABLE_ALIAS_LITERAL(5) +SQLGEN_TABLE_ALIAS_LITERAL(6) +SQLGEN_TABLE_ALIAS_LITERAL(7) +SQLGEN_TABLE_ALIAS_LITERAL(8) +SQLGEN_TABLE_ALIAS_LITERAL(9) +SQLGEN_TABLE_ALIAS_LITERAL(10) +SQLGEN_TABLE_ALIAS_LITERAL(11) +SQLGEN_TABLE_ALIAS_LITERAL(12) +SQLGEN_TABLE_ALIAS_LITERAL(13) +SQLGEN_TABLE_ALIAS_LITERAL(14) +SQLGEN_TABLE_ALIAS_LITERAL(15) +SQLGEN_TABLE_ALIAS_LITERAL(16) +SQLGEN_TABLE_ALIAS_LITERAL(17) +SQLGEN_TABLE_ALIAS_LITERAL(18) +SQLGEN_TABLE_ALIAS_LITERAL(19) +SQLGEN_TABLE_ALIAS_LITERAL(20) +SQLGEN_TABLE_ALIAS_LITERAL(21) +SQLGEN_TABLE_ALIAS_LITERAL(22) +SQLGEN_TABLE_ALIAS_LITERAL(23) +SQLGEN_TABLE_ALIAS_LITERAL(24) +SQLGEN_TABLE_ALIAS_LITERAL(25) +SQLGEN_TABLE_ALIAS_LITERAL(26) +SQLGEN_TABLE_ALIAS_LITERAL(27) +SQLGEN_TABLE_ALIAS_LITERAL(28) +SQLGEN_TABLE_ALIAS_LITERAL(29) +SQLGEN_TABLE_ALIAS_LITERAL(30) +SQLGEN_TABLE_ALIAS_LITERAL(31) +SQLGEN_TABLE_ALIAS_LITERAL(32) +SQLGEN_TABLE_ALIAS_LITERAL(33) +SQLGEN_TABLE_ALIAS_LITERAL(34) +SQLGEN_TABLE_ALIAS_LITERAL(35) +SQLGEN_TABLE_ALIAS_LITERAL(36) +SQLGEN_TABLE_ALIAS_LITERAL(37) +SQLGEN_TABLE_ALIAS_LITERAL(38) +SQLGEN_TABLE_ALIAS_LITERAL(39) +SQLGEN_TABLE_ALIAS_LITERAL(40) +SQLGEN_TABLE_ALIAS_LITERAL(41) +SQLGEN_TABLE_ALIAS_LITERAL(42) +SQLGEN_TABLE_ALIAS_LITERAL(43) +SQLGEN_TABLE_ALIAS_LITERAL(44) +SQLGEN_TABLE_ALIAS_LITERAL(45) +SQLGEN_TABLE_ALIAS_LITERAL(46) +SQLGEN_TABLE_ALIAS_LITERAL(47) +SQLGEN_TABLE_ALIAS_LITERAL(48) +SQLGEN_TABLE_ALIAS_LITERAL(49) +SQLGEN_TABLE_ALIAS_LITERAL(50) +SQLGEN_TABLE_ALIAS_LITERAL(51) +SQLGEN_TABLE_ALIAS_LITERAL(52) +SQLGEN_TABLE_ALIAS_LITERAL(53) +SQLGEN_TABLE_ALIAS_LITERAL(54) +SQLGEN_TABLE_ALIAS_LITERAL(55) +SQLGEN_TABLE_ALIAS_LITERAL(56) +SQLGEN_TABLE_ALIAS_LITERAL(57) +SQLGEN_TABLE_ALIAS_LITERAL(58) +SQLGEN_TABLE_ALIAS_LITERAL(59) +SQLGEN_TABLE_ALIAS_LITERAL(60) +SQLGEN_TABLE_ALIAS_LITERAL(61) +SQLGEN_TABLE_ALIAS_LITERAL(62) +SQLGEN_TABLE_ALIAS_LITERAL(63) +SQLGEN_TABLE_ALIAS_LITERAL(64) +SQLGEN_TABLE_ALIAS_LITERAL(65) +SQLGEN_TABLE_ALIAS_LITERAL(66) +SQLGEN_TABLE_ALIAS_LITERAL(67) +SQLGEN_TABLE_ALIAS_LITERAL(68) +SQLGEN_TABLE_ALIAS_LITERAL(69) +SQLGEN_TABLE_ALIAS_LITERAL(70) +SQLGEN_TABLE_ALIAS_LITERAL(71) +SQLGEN_TABLE_ALIAS_LITERAL(72) +SQLGEN_TABLE_ALIAS_LITERAL(73) +SQLGEN_TABLE_ALIAS_LITERAL(74) +SQLGEN_TABLE_ALIAS_LITERAL(75) +SQLGEN_TABLE_ALIAS_LITERAL(76) +SQLGEN_TABLE_ALIAS_LITERAL(77) +SQLGEN_TABLE_ALIAS_LITERAL(78) +SQLGEN_TABLE_ALIAS_LITERAL(79) +SQLGEN_TABLE_ALIAS_LITERAL(80) +SQLGEN_TABLE_ALIAS_LITERAL(81) +SQLGEN_TABLE_ALIAS_LITERAL(82) +SQLGEN_TABLE_ALIAS_LITERAL(83) +SQLGEN_TABLE_ALIAS_LITERAL(84) +SQLGEN_TABLE_ALIAS_LITERAL(85) +SQLGEN_TABLE_ALIAS_LITERAL(86) +SQLGEN_TABLE_ALIAS_LITERAL(87) +SQLGEN_TABLE_ALIAS_LITERAL(88) +SQLGEN_TABLE_ALIAS_LITERAL(89) +SQLGEN_TABLE_ALIAS_LITERAL(90) +SQLGEN_TABLE_ALIAS_LITERAL(91) +SQLGEN_TABLE_ALIAS_LITERAL(92) +SQLGEN_TABLE_ALIAS_LITERAL(93) +SQLGEN_TABLE_ALIAS_LITERAL(94) +SQLGEN_TABLE_ALIAS_LITERAL(95) +SQLGEN_TABLE_ALIAS_LITERAL(96) +SQLGEN_TABLE_ALIAS_LITERAL(97) +SQLGEN_TABLE_ALIAS_LITERAL(98) +SQLGEN_TABLE_ALIAS_LITERAL(99) } // namespace sqlgen diff --git a/include/sqlgen/dynamic/Join.hpp b/include/sqlgen/dynamic/Join.hpp new file mode 100644 index 0000000..fd94bc4 --- /dev/null +++ b/include/sqlgen/dynamic/Join.hpp @@ -0,0 +1,12 @@ +#ifndef SQLGEN_DYNAMIC_JOIN_HPP_ +#define SQLGEN_DYNAMIC_JOIN_HPP_ + +#include "SelectFrom.hpp" + +namespace sqlgen::dynamic { + +using Join = SelectFrom::Join; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/dynamic/JoinType.hpp b/include/sqlgen/dynamic/JoinType.hpp new file mode 100644 index 0000000..a82f55e --- /dev/null +++ b/include/sqlgen/dynamic/JoinType.hpp @@ -0,0 +1,10 @@ +#ifndef SQLGEN_DYNAMIC_JOINTYPE_HPP_ +#define SQLGEN_DYNAMIC_JOINTYPE_HPP_ + +namespace sqlgen::dynamic { + +enum class JoinType { inner_join, left_join, right_join, full_join }; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/dynamic/SelectFrom.hpp b/include/sqlgen/dynamic/SelectFrom.hpp index 6876a96..7ec8537 100644 --- a/include/sqlgen/dynamic/SelectFrom.hpp +++ b/include/sqlgen/dynamic/SelectFrom.hpp @@ -6,8 +6,10 @@ #include #include +#include "../Ref.hpp" #include "Condition.hpp" #include "GroupBy.hpp" +#include "JoinType.hpp" #include "Limit.hpp" #include "Operation.hpp" #include "OrderBy.hpp" @@ -21,8 +23,17 @@ struct SelectFrom { std::optional as; }; + struct Join { + JoinType how; + rfl::Variant> table_or_query; + std::string alias; + std::optional on; + }; + Table table; std::vector fields; + std::optional alias = std::nullopt; + std::optional> joins = std::nullopt; std::optional where = std::nullopt; std::optional group_by = std::nullopt; std::optional order_by = std::nullopt; diff --git a/include/sqlgen/internal/strings/strings.hpp b/include/sqlgen/internal/strings/strings.hpp index 9d45571..ea290f5 100644 --- a/include/sqlgen/internal/strings/strings.hpp +++ b/include/sqlgen/internal/strings/strings.hpp @@ -8,8 +8,12 @@ namespace sqlgen::internal::strings { char to_lower(const char ch); +std::string to_lower(const std::string& _str); + char to_upper(const char ch); +std::string to_upper(const std::string& _str); + std::string join(const std::string& _delimiter, const std::vector& _strings); diff --git a/include/sqlgen/joins.hpp b/include/sqlgen/joins.hpp new file mode 100644 index 0000000..5164fd2 --- /dev/null +++ b/include/sqlgen/joins.hpp @@ -0,0 +1,72 @@ +#ifndef SQLGEN_JOINS_HPP_ +#define SQLGEN_JOINS_HPP_ + +#include + +#include "transpilation/Join.hpp" + +namespace sqlgen { + +template +auto join(const transpilation::JoinType _how, + const TableOrQueryType& _table_or_query, const ConditionType& _on) { + return transpilation::Join{ + .how = _how, .table_or_query = _table_or_query, .on = _on}; +}; + +template +auto full_join(const QueryType& _query, const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::full_join, _query, _on); +} + +template +auto full_join(const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::full_join, + transpilation::TableWrapper{}, _on); +} + +template +auto inner_join(const QueryType& _query, const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::inner_join, _query, _on); +} + +template +auto inner_join(const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::inner_join, + transpilation::TableWrapper{}, _on); +} + +template +auto left_join(const QueryType& _query, const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::left_join, _query, _on); +} + +template +auto left_join(const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::left_join, + transpilation::TableWrapper{}, _on); +} + +template +auto right_join(const QueryType& _query, const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::right_join, _query, _on); +} + +template +auto right_join(const ConditionType& _on) { + return join<_alias>(transpilation::JoinType::right_join, + transpilation::TableWrapper{}, _on); +} + +} // namespace sqlgen + +#endif diff --git a/include/sqlgen/select_from.hpp b/include/sqlgen/select_from.hpp index 4ad8946..af4f1a6 100644 --- a/include/sqlgen/select_from.hpp +++ b/include/sqlgen/select_from.hpp @@ -9,6 +9,8 @@ #include "Ref.hpp" #include "Result.hpp" #include "col.hpp" +#include "dynamic/Join.hpp" +#include "dynamic/SelectFrom.hpp" #include "group_by.hpp" #include "internal/GetColType.hpp" #include "internal/is_range.hpp" @@ -16,26 +18,32 @@ #include "limit.hpp" #include "order_by.hpp" #include "to.hpp" +#include "transpilation/Join.hpp" #include "transpilation/fields_to_named_tuple_t.hpp" #include "transpilation/group_by_t.hpp" #include "transpilation/order_by_t.hpp" +#include "transpilation/table_tuple_t.hpp" +#include "transpilation/to_joins.hpp" #include "transpilation/to_select_from.hpp" #include "transpilation/value_t.hpp" #include "where.hpp" namespace sqlgen { -template +template requires is_connection auto select_from_impl(const Ref& _conn, const FieldsType& _fields, - const WhereType& _where, const LimitType& _limit) { + const JoinsType& _joins, const WhereType& _where, + const LimitType& _limit) { if constexpr (internal::is_range_v) { const auto query = - transpilation::to_select_from( - _fields, _where, _limit); + transpilation::to_select_from(_fields, _joins, + _where, _limit); return _conn->read(query).transform( [](auto&& _it) { return ContainerType(_it); }); @@ -54,44 +62,51 @@ auto select_from_impl(const Ref& _conn, const FieldsType& _fields, return container; }; - using RangeType = - Range>; + using RangeType = Range< + transpilation::fields_to_named_tuple_t>; - return select_from_impl(_conn, _fields, - _where, _limit) + return select_from_impl(_conn, _fields, _joins, _where, _limit) .and_then(to_container); } } -template +template requires is_connection auto select_from_impl(const Result>& _res, - const FieldsType& _fields, const WhereType& _where, - const LimitType& _limit) { + const FieldsType& _fields, const JoinsType& _joins, + const WhereType& _where, const LimitType& _limit) { return _res.and_then([&](const auto& _conn) { - return select_from_impl( - _conn, _where, _limit); + return select_from_impl(_conn, _joins, _where, _limit); }); } -template struct SelectFrom { auto operator()(const auto& _conn) const { + using TableTupleType = + transpilation::table_tuple_t; + if constexpr (std::is_same_v || std::ranges::input_range>) { - using ContainerType = std::conditional_t< - std::is_same_v, - Range>, - ToType>; - return select_from_impl( - _conn, fields_, where_, limit_); + using ContainerType = + std::conditional_t, + Range>, + ToType>; + return select_from_impl(_conn, fields_, joins_, where_, + limit_); } else { const auto extract_result = [](auto&& _vec) -> Result { @@ -104,14 +119,42 @@ struct SelectFrom { return std::move(_vec[0]); }; - return select_from_impl>>( - _conn, fields_, where_, limit_) + _conn, fields_, joins_, where_, limit_) .and_then(extract_result); } } + template + friend auto operator|( + const SelectFrom& _s, + const transpilation::Join& + _join) { + if constexpr (std::is_same_v) { + using NewJoinsType = rfl::Tuple< + transpilation::Join>; + + return SelectFrom{ + .fields_ = _s.fields_, .joins_ = NewJoinsType(_join)}; + + } else { + using TupleType = rfl::Tuple< + transpilation::Join>; + + const auto joins = rfl::tuple_cat(_s.joins_, TupleType(_join)); + + using NewJoinsType = std::remove_cvref_t; + + return SelectFrom{ + .fields_ = _s.fields_, .joins_ = joins}; + } + } + template friend auto operator|(const SelectFrom& _s, const Where& _where) { @@ -124,9 +167,10 @@ struct SelectFrom { "You cannot call limit(...) before where(...)."); static_assert(std::is_same_v, "You cannot call to<...> before where(...)."); - return SelectFrom{ - .fields_ = _s.fields_, .where_ = _where.condition}; + return SelectFrom{ + .fields_ = _s.fields_, .joins_ = _s.joins_, .where_ = _where.condition}; } template @@ -144,10 +188,10 @@ struct SelectFrom { static_assert(sizeof...(ColTypes) != 0, "You must assign at least one column to group_by."); return SelectFrom< - StructType, FieldsType, WhereType, + StructType, AliasType, FieldsType, JoinsType, WhereType, transpilation::group_by_t, - OrderByType, LimitType, ToType>{.fields_ = _s.fields_, - .where_ = _s.where_}; + OrderByType, LimitType, ToType>{ + .fields_ = _s.fields_, .joins_ = _s.joins_, .where_ = _s.where_}; } template @@ -163,41 +207,101 @@ struct SelectFrom { static_assert(sizeof...(ColTypes) != 0, "You must assign at least one column to order_by."); return SelectFrom< - StructType, FieldsType, WhereType, GroupByType, + StructType, AliasType, FieldsType, JoinsType, WhereType, GroupByType, transpilation::order_by_t< StructType, typename std::remove_cvref_t::ColType...>, - LimitType, ToType>{.fields_ = _s.fields_, .where_ = _s.where_}; + LimitType, ToType>{ + .fields_ = _s.fields_, .joins_ = _s.joins_, .where_ = _s.where_}; } friend auto operator|(const SelectFrom& _s, const Limit& _limit) { static_assert(std::is_same_v, "You cannot call limit twice."); - return SelectFrom{ - .fields_ = _s.fields_, .where_ = _s.where_, .limit_ = _limit}; + return SelectFrom{ + .fields_ = _s.fields_, + .joins_ = _s.joins_, + .where_ = _s.where_, + .limit_ = _limit}; } template friend auto operator|(const SelectFrom& _s, const To&) { static_assert(std::is_same_v, "You cannot call to<...> twice."); - return SelectFrom{ - .fields_ = _s.fields_, .where_ = _s.where_, .limit_ = _s.limit_}; + return SelectFrom{ + .fields_ = _s.fields_, + .joins_ = _s.joins_, + .where_ = _s.where_, + .limit_ = _s.limit_}; } FieldsType fields_; + JoinsType joins_; + WhereType where_; LimitType limit_; }; +namespace transpilation { + +template +struct ExtractTable< + SelectFrom> { + using TableTupleType = table_tuple_t; + using Type = fields_to_named_tuple_t; +}; + +template +struct ToJoin> { + template + dynamic::Join operator()( + const transpilation::Join< + SelectFrom, + ConditionType, _alias>& _join) { + const auto& query = _join.table_or_query; + + using NestedTableTupleType = + table_tuple_t; + + return dynamic::Join{ + .how = _join.how, + .table_or_query = Ref::make( + transpilation::to_select_from( + query.fields_, query.joins_, query.where_, query.limit_)), + .alias = Literal<_alias>().str(), + .on = to_condition(_join.on)}; + } +}; + +} // namespace transpilation + template inline auto select_from(const FieldTypes&... _fields) { using FieldsType = rfl::Tuple::Type...>; - return SelectFrom{ + return SelectFrom{ + .fields_ = + FieldsType(internal::GetColType::get_value(_fields)...)}; +} + +template +inline auto select_from(const FieldTypes&... _fields) { + using FieldsType = + rfl::Tuple::Type...>; + return SelectFrom, FieldsType>{ .fields_ = FieldsType(internal::GetColType::get_value(_fields)...)}; } diff --git a/include/sqlgen/transpilation/Col.hpp b/include/sqlgen/transpilation/Col.hpp index e100054..4673933 100644 --- a/include/sqlgen/transpilation/Col.hpp +++ b/include/sqlgen/transpilation/Col.hpp @@ -6,10 +6,15 @@ namespace sqlgen::transpilation { -template +template struct Col { - using ColType = Col<_name>; + using ColType = Col<_name, _alias>; using Name = rfl::Literal<_name>; + using Alias = rfl::Literal<_alias>; + + /// Returns the column alias. + std::string alias() const noexcept { return Alias().str(); } /// Returns the column name. std::string name() const noexcept { return Name().str(); } diff --git a/include/sqlgen/transpilation/Join.hpp b/include/sqlgen/transpilation/Join.hpp new file mode 100644 index 0000000..bd9a020 --- /dev/null +++ b/include/sqlgen/transpilation/Join.hpp @@ -0,0 +1,27 @@ +#ifndef SQLGEN_TRANSPILATION_JOIN_HPP_ +#define SQLGEN_TRANSPILATION_JOIN_HPP_ + +#include + +#include "../Literal.hpp" +#include "../dynamic/JoinType.hpp" +#include "TableWrapper.hpp" + +namespace sqlgen::transpilation { + +using JoinType = dynamic::JoinType; + +template +struct Join { + using TableOrQueryType = _TableOrQueryType; + using Alias = Literal<_alias>; + + JoinType how; + TableOrQueryType table_or_query; + ConditionType on; +}; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/TableWrapper.hpp b/include/sqlgen/transpilation/TableWrapper.hpp new file mode 100644 index 0000000..12b92c0 --- /dev/null +++ b/include/sqlgen/transpilation/TableWrapper.hpp @@ -0,0 +1,13 @@ +#ifndef SQLGEN_TRANSPILATION_TABLEWRAPPER_HPP_ +#define SQLGEN_TRANSPILATION_TABLEWRAPPER_HPP_ + +namespace sqlgen::transpilation { + +template +struct TableWrapper { + using TableType = _TableType; +}; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/all_columns_exist.hpp b/include/sqlgen/transpilation/all_columns_exist.hpp index b176928..b9f4b1c 100644 --- a/include/sqlgen/transpilation/all_columns_exist.hpp +++ b/include/sqlgen/transpilation/all_columns_exist.hpp @@ -6,26 +6,32 @@ #include "../Literal.hpp" #include "Col.hpp" +#include "get_table_t.hpp" namespace sqlgen::transpilation { -template +template struct ColumnExists; -template -struct ColumnExists, transpilation::Col<_col_name>> { +struct ColumnExists, + transpilation::Col<_col_name, _alias>> { static constexpr bool value = (false || ... || (_col_name == _field_names)); static_assert(value, "Column does not exist."); }; -template -constexpr bool column_exists_v = ColumnExists::value; +template +constexpr bool column_exists_v = ColumnExists::value; -template +template consteval bool all_columns_exist() { - using Names = typename rfl::named_tuple_t>::Names; - return (true && ... && column_exists_v); + return (true && ... && + column_exists_v, + typename rfl::named_tuple_t>::Names, + ColTypes>); } } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/extract_table_t.hpp b/include/sqlgen/transpilation/extract_table_t.hpp new file mode 100644 index 0000000..4fa6ee6 --- /dev/null +++ b/include/sqlgen/transpilation/extract_table_t.hpp @@ -0,0 +1,28 @@ +#ifndef SQLGEN_TRANSPILATION_EXTRACTTABLET_HPP_ +#define SQLGEN_TRANSPILATION_EXTRACTTABLET_HPP_ + +#include + +#include "TableWrapper.hpp" + +namespace sqlgen::transpilation { + +template +struct ExtractTable; + +template +struct ExtractTable { + using Type = T; +}; + +template +struct ExtractTable> { + using Type = typename ExtractTable>::Type; +}; + +template +using extract_table_t = typename ExtractTable>::Type; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/get_table_t.hpp b/include/sqlgen/transpilation/get_table_t.hpp new file mode 100644 index 0000000..306034f --- /dev/null +++ b/include/sqlgen/transpilation/get_table_t.hpp @@ -0,0 +1,74 @@ +#ifndef SQLGEN_TRANSPILATION_GETTABLET_HPP_ +#define SQLGEN_TRANSPILATION_GETTABLET_HPP_ + +#include +#include +#include +#include + +#include "../Literal.hpp" +#include "../dynamic/JoinType.hpp" + +namespace sqlgen::transpilation { + +template +struct PairWrapper { + using TableType = _TableType; + using AliasType = _AliasType; + + template + friend consteval auto operator||( + const PairWrapper&, + const PairWrapper&) noexcept { + if constexpr (std::is_same_v>) { + return PairWrapper{}; + } else { + return PairWrapper{}; + } + } +}; + +template +struct GetTableType; + +template +struct GetTableType, + rfl::Tuple...>> { + static constexpr auto wrapper = + (PairWrapper, Nothing, Nothing>{} || ... || + PairWrapper, TableTypes, AliasTypes>{}); + + using TableType = std::remove_cvref_t; + + static_assert(!std::is_same_v, + "Alias could not be identified."); +}; + +template +struct GetTableType, T> { + using TableType = T; +}; + +template +struct GetTableType, + rfl::Tuple...>> { + using TableType = typename rfl::tuple_element_t< + _i, rfl::Tuple...>>::first_type; + + static_assert(!std::is_same_v, + "Alias could not be identified."); +}; + +template +struct GetTableType, T> { + using TableType = T; +}; + +template +using get_table_t = typename GetTableType, + std::remove_cvref_t>::TableType; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index a7d5dbe..92d8cb0 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -20,19 +20,21 @@ #include "all_columns_exist.hpp" #include "dynamic_aggregation_t.hpp" #include "dynamic_operator_t.hpp" +#include "get_table_t.hpp" #include "is_timestamp.hpp" #include "remove_as_t.hpp" #include "remove_nullable_t.hpp" +#include "to_alias.hpp" #include "to_duration.hpp" #include "to_value.hpp" #include "underlying_t.hpp" namespace sqlgen::transpilation { -template +template struct MakeField; -template +template struct MakeField { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; @@ -47,26 +49,31 @@ struct MakeField { } }; -template -struct MakeField> { - static_assert(all_columns_exist>(), +template +struct MakeField> { + static_assert(all_columns_exist>(), "A required column does not exist."); static constexpr bool is_aggregation = false; static constexpr bool is_column = true; static constexpr bool is_operation = false; + using TableType = + get_table_t::Alias, TableTupleType>; + using Name = Literal<_name>; - using Type = rfl::field_type_t<_name, StructType>; + using Type = rfl::field_type_t<_name, TableType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{.val = dynamic::Column{.name = _name.str()}}}; + return dynamic::SelectFrom::Field{dynamic::Operation{ + .val = dynamic::Column{.alias = to_alias>(), + .name = _name.str()}}}; } }; -template -struct MakeField> { +template +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = false; @@ -80,37 +87,40 @@ struct MakeField> { } }; -template -struct MakeField> { +struct MakeField> { static constexpr bool is_aggregation = - MakeField::is_aggregation; - static constexpr bool is_column = MakeField::is_column; + MakeField::is_aggregation; + static constexpr bool is_column = + MakeField::is_column; static constexpr bool is_operation = - MakeField::is_operation; + MakeField::is_operation; using Name = Literal<_new_name>; using Type = - typename MakeField>::Type; + typename MakeField>::Type; dynamic::SelectFrom::Field operator()(const auto& _as) const { return dynamic::SelectFrom::Field{ .val = dynamic::Operation{ - .val = MakeField>{}( - _as.val) - .val.val}, + .val = + MakeField>{}( + _as.val) + .val.val}, .as = _new_name.str()}; } }; -template -struct MakeField> { - static_assert(std::is_integral_v< - remove_nullable_t>> || - std::is_floating_point_v< - remove_nullable_t>>, - "Values inside the aggregation must be numerical."); +template +struct MakeField> { + static_assert( + std::is_integral_v< + remove_nullable_t>> || + std::is_floating_point_v< + remove_nullable_t>>, + "Values inside the aggregation must be numerical."); // Recursively checks if a type contains any aggregations. template @@ -118,18 +128,18 @@ struct MakeField> { // Case: No operation. template - requires(!MakeField>::is_operation) + requires(!MakeField>::is_operation) struct HasAggregations { static constexpr bool value = - MakeField>::is_aggregation; + MakeField>::is_aggregation; }; // Case: Is operations: Check all operands. template - requires(MakeField>::is_operation) + requires(MakeField>::is_operation) struct HasAggregations { static constexpr bool value = HasAggregations< - typename MakeField>::Operands>::value; + typename MakeField>::Operands>::value; }; // Case: Is a set of operands from an operation.. @@ -149,22 +159,24 @@ struct MakeField> { using Name = Nothing; using Type = - typename MakeField>::Type; + typename MakeField>::Type; dynamic::SelectFrom::Field operator()(const auto& _val) const { using DynamicAggregationType = dynamic_aggregation_t<_agg>; return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{DynamicAggregationType{ .val = Ref::make( - MakeField>{}( + MakeField>{}( _val.val) .val)}}}}; } }; -template -struct MakeField>> { - static_assert(all_columns_exist>(), +template +struct MakeField>> { + static_assert(all_columns_exist>(), "A column required in the COUNT or COUNT_DISTINCT aggregation " "does not exist."); @@ -178,13 +190,14 @@ struct MakeField>> { dynamic::SelectFrom::Field operator()(const auto& _agg) const { return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Count{ - .val = dynamic::Column{.name = _name.str()}, + .val = dynamic::Column{.alias = to_alias>(), + .name = _name.str()}, .distinct = _agg.distinct}}}}; } }; -template -struct MakeField> { +template +struct MakeField> { static constexpr bool is_aggregation = true; static constexpr bool is_column = true; static constexpr bool is_operation = false; @@ -200,9 +213,9 @@ struct MakeField> { } }; -template -struct MakeField>> { +template +struct MakeField>> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; @@ -215,7 +228,7 @@ struct MakeField::make( - MakeField>{}( + MakeField>{}( _o.operand1) .val), .target_type = @@ -223,8 +236,8 @@ struct MakeField -struct MakeField +struct MakeField>> { static constexpr bool is_aggregation = false; @@ -232,7 +245,7 @@ struct MakeField>; + using Type = underlying_t>; using Operands = rfl::Tuple; static_assert(is_timestamp_v>, @@ -242,7 +255,7 @@ struct MakeField::make( - MakeField>{}( + MakeField>{}( _o.operand1) .val), .durations = rfl::apply( @@ -253,58 +266,58 @@ struct MakeField +template requires((num_operands_v<_op>) == 1) -struct MakeField> { +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; using Name = Nothing; - using Type = underlying_t>; + using Type = underlying_t>; using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ .op1 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand1) .val)}}}; } }; -template requires((num_operands_v<_op>) == 2) -struct MakeField> { +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; using Name = Nothing; using Type = - underlying_t>; + underlying_t>; using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ .op1 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand1) .val), .op2 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand2) .val)}}}; } }; -template requires((num_operands_v<_op>) == 3) -struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; @@ -312,38 +325,38 @@ struct MakeField>; + underlying_t>; using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { return dynamic::SelectFrom::Field{ dynamic::Operation{dynamic::Operation::Replace{ .op1 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand1) .val), .op2 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand2) .val), .op3 = Ref::make( - MakeField>{}( + MakeField>{}( _o.operand3) .val)}}}; } }; -template +template requires((num_operands_v<_op>) == std::numeric_limits::max()) -struct MakeField>> { +struct MakeField>> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; using Name = Nothing; using Type = - underlying_t>>; + underlying_t>>; using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { @@ -353,7 +366,7 @@ struct MakeField>> { [](const auto&... _ops) { return std::vector>( {Ref::make( - MakeField>{}(_ops) .val)...}); }, @@ -361,9 +374,9 @@ struct MakeField>> { } }; -template +template inline dynamic::SelectFrom::Field make_field(const ValueType& _val) { - return MakeField, + return MakeField, std::remove_cvref_t>{}(_val); } diff --git a/include/sqlgen/transpilation/make_fields.hpp b/include/sqlgen/transpilation/make_fields.hpp index 47c0f04..1ef5e64 100644 --- a/include/sqlgen/transpilation/make_fields.hpp +++ b/include/sqlgen/transpilation/make_fields.hpp @@ -10,11 +10,11 @@ namespace sqlgen::transpilation { -template +template std::vector make_fields( const Fields& _fields, std::integer_sequence) { return std::vector( - {make_field(rfl::get<_is>(_fields))...}); + {make_field(rfl::get<_is>(_fields))...}); } } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/order_by_t.hpp b/include/sqlgen/transpilation/order_by_t.hpp index bcd67dd..92709f3 100644 --- a/include/sqlgen/transpilation/order_by_t.hpp +++ b/include/sqlgen/transpilation/order_by_t.hpp @@ -14,15 +14,17 @@ namespace sqlgen::transpilation { template struct OrderByWrapper; -template -struct OrderByWrapper> { - using ColType = transpilation::Col<_name>; +template +struct OrderByWrapper> { + using ColType = transpilation::Col<_name, _alias>; constexpr static bool desc = false; }; -template -struct OrderByWrapper>> { - using ColType = transpilation::Col<_name>; +template +struct OrderByWrapper>> { + using ColType = transpilation::Col<_name, _alias>; constexpr static bool desc = true; }; diff --git a/include/sqlgen/transpilation/table_tuple_t.hpp b/include/sqlgen/transpilation/table_tuple_t.hpp new file mode 100644 index 0000000..46d5464 --- /dev/null +++ b/include/sqlgen/transpilation/table_tuple_t.hpp @@ -0,0 +1,38 @@ +#ifndef SQLGEN_TRANSPILATION_TABLETUPLET_HPP_ +#define SQLGEN_TRANSPILATION_TABLETUPLET_HPP_ + +#include +#include +#include + +#include "../Literal.hpp" +#include "../Result.hpp" +#include "extract_table_t.hpp" + +namespace sqlgen::transpilation { + +template +struct TableTupleType; + +template +struct TableTupleType { + using Type = StructType; +}; + +template +struct TableTupleType> { + using Type = rfl::Tuple< + std::pair, AliasType>, + std::pair, + typename JoinTypes::Alias>...>; +}; + +template +using table_tuple_t = + typename TableTupleType, + std::remove_cvref_t, + std::remove_cvref_t>::Type; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_alias.hpp b/include/sqlgen/transpilation/to_alias.hpp new file mode 100644 index 0000000..bd4f480 --- /dev/null +++ b/include/sqlgen/transpilation/to_alias.hpp @@ -0,0 +1,41 @@ +#ifndef SQLGEN_TRANSPILATION_TO_ALIAS_HPP_ +#define SQLGEN_TRANSPILATION_TO_ALIAS_HPP_ + +#include +#include +#include +#include + +#include "../Literal.hpp" +#include "../Result.hpp" + +namespace sqlgen::transpilation { + +template +struct ToAlias; + +template <> +struct ToAlias { + std::optional operator()() const { return std::nullopt; } +}; + +template <> +struct ToAlias> { + std::optional operator()() const { return std::nullopt; } +}; + +template +struct ToAlias> { + std::optional operator()() const { + return Literal<_alias>().str(); + } +}; + +template +std::optional to_alias() { + return ToAlias>{}(); +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_group_by.hpp b/include/sqlgen/transpilation/to_group_by.hpp index e9f3b39..7e8ffa9 100644 --- a/include/sqlgen/transpilation/to_group_by.hpp +++ b/include/sqlgen/transpilation/to_group_by.hpp @@ -8,6 +8,7 @@ #include "../dynamic/Column.hpp" #include "../dynamic/GroupBy.hpp" #include "group_by_t.hpp" +#include "to_alias.hpp" namespace sqlgen::transpilation { @@ -23,7 +24,8 @@ template struct ToGroupBy> { std::optional operator()() const { const auto columns = std::vector( - {dynamic::Column{.name = ColTypes().name()}...}); + {dynamic::Column{.alias = to_alias(), + .name = ColTypes().name()}...}); return dynamic::GroupBy{.columns = columns}; } }; diff --git a/include/sqlgen/transpilation/to_joins.hpp b/include/sqlgen/transpilation/to_joins.hpp new file mode 100644 index 0000000..0085338 --- /dev/null +++ b/include/sqlgen/transpilation/to_joins.hpp @@ -0,0 +1,63 @@ +#ifndef SQLGEN_TRANSPILATION_TO_JOIN_HPP_ +#define SQLGEN_TRANSPILATION_TO_JOIN_HPP_ + +#include +#include +#include +#include + +#include "../dynamic/Join.hpp" +#include "../dynamic/Table.hpp" +#include "Join.hpp" +#include "TableWrapper.hpp" +#include "get_schema.hpp" +#include "get_tablename.hpp" +#include "to_condition.hpp" + +namespace sqlgen::transpilation { + +template +struct ToJoin; + +template +struct ToJoin> { + template + dynamic::Join operator()( + const Join, ConditionType, _alias>& _join) { + using T = std::remove_cvref_t; + using Alias = + typename Join, ConditionType, _alias>::Alias; + + return dynamic::Join{ + .how = _join.how, + .table_or_query = dynamic::Table{.name = get_tablename(), + .schema = get_schema()}, + .alias = Alias().str(), + .on = to_condition(_join.on)}; + } +}; + +template +dynamic::Join to_join(const Join& _join) { + return ToJoin{}(_join); +} + +template +std::optional> to_joins( + const rfl::Tuple& _joins) { + return rfl::apply( + [](const auto&... _js) { + return std::vector({to_join(_js)...}); + }, + _joins); +} + +template +inline std::optional> to_joins(const Nothing&) { + return std::nullopt; +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_order_by.hpp b/include/sqlgen/transpilation/to_order_by.hpp index af483b2..cf54898 100644 --- a/include/sqlgen/transpilation/to_order_by.hpp +++ b/include/sqlgen/transpilation/to_order_by.hpp @@ -6,6 +6,7 @@ #include "../Result.hpp" #include "../dynamic/OrderBy.hpp" #include "order_by_t.hpp" +#include "to_alias.hpp" namespace sqlgen::transpilation { @@ -21,8 +22,9 @@ template struct ToOrderBy> { template dynamic::Wrapper to_wrapper() const { + using Alias = typename W::ColType::Alias; using Name = typename W::ColType::Name; - const auto column = dynamic::Column{.alias = std::nullopt, + const auto column = dynamic::Column{.alias = to_alias(), .name = Name().str(), .type = dynamic::types::Unknown{}}; return dynamic::Wrapper{.column = column, .desc = W::desc}; diff --git a/include/sqlgen/transpilation/to_select_from.hpp b/include/sqlgen/transpilation/to_select_from.hpp index b0bf6b2..62df16d 100644 --- a/include/sqlgen/transpilation/to_select_from.hpp +++ b/include/sqlgen/transpilation/to_select_from.hpp @@ -10,43 +10,54 @@ #include "../Result.hpp" #include "../dynamic/ColumnOrAggregation.hpp" +#include "../dynamic/Join.hpp" #include "../dynamic/SelectFrom.hpp" #include "../dynamic/Table.hpp" #include "../internal/collect/vector.hpp" +#include "Join.hpp" #include "check_aggregations.hpp" #include "flatten_fields_t.hpp" #include "get_schema.hpp" +#include "get_table_t.hpp" #include "get_tablename.hpp" #include "make_fields.hpp" +#include "to_alias.hpp" #include "to_condition.hpp" #include "to_group_by.hpp" +#include "to_joins.hpp" #include "to_limit.hpp" #include "to_order_by.hpp" namespace sqlgen::transpilation { -template - requires std::is_class_v> && - std::is_aggregate_v> +template dynamic::SelectFrom to_select_from(const FieldsType& _fields, + const JoinsType& _joins, const WhereType& _where, const LimitType& _limit) { - static_assert( - check_aggregations, - GroupByType>(), - "The aggregations were not set up correctly. Please check the " - "trace for a more detailed error message."); + static_assert(check_aggregations, + GroupByType>(), + "The aggregations were not set up correctly. Please check the " + "trace for a more detailed error message."); - const auto fields = make_fields( + using StructType = + get_table_t, TableTupleType>; + + const auto fields = make_fields( _fields, std::make_integer_sequence>()); return dynamic::SelectFrom{ - .table = dynamic::Table{.name = get_tablename(), + .table = dynamic::Table{.alias = to_alias(), + .name = get_tablename(), .schema = get_schema()}, .fields = fields, - .where = to_condition>(_where), + .alias = to_alias(), + .joins = to_joins(_joins), + .where = to_condition>(_where), .group_by = to_group_by(), .order_by = to_order_by(), .limit = to_limit(_limit)}; diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index a84309d..adb0146 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -14,6 +14,7 @@ #include "../update.hpp" #include "columns_t.hpp" #include "read_to_select_from.hpp" +#include "table_tuple_t.hpp" #include "to_create_index.hpp" #include "to_create_table.hpp" #include "to_delete_from.hpp" @@ -76,14 +77,17 @@ struct ToSQL> { } }; -template -struct ToSQL> { +template +struct ToSQL> { dynamic::Statement operator()(const auto& _select_from) const { - return to_select_from( - _select_from.fields_, _select_from.where_, _select_from.limit_); + using TableTupleType = table_tuple_t; + return to_select_from( + _select_from.fields_, _select_from.joins_, _select_from.where_, + _select_from.limit_); } }; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 213c3e9..910a9bb 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -13,6 +13,7 @@ #include "Value.hpp" #include "all_columns_exist.hpp" #include "dynamic_operator_t.hpp" +#include "get_table_t.hpp" #include "is_nullable.hpp" #include "is_timestamp.hpp" #include "remove_nullable_t.hpp" @@ -20,72 +21,85 @@ namespace sqlgen::transpilation { -template +template struct Underlying; -template -struct Underlying> { - using Type = typename Underlying>::Type; -}; - -template -struct Underlying> { - static_assert(all_columns_exist>(), "All columns must exist."); - using Type = remove_reflection_t>; -}; - -template -struct Underlying>> { - using Type = remove_reflection_t>; -}; - -template -struct Underlying< - T, Operation>> { +template +struct Underlying> { using Type = - std::conditional_t>::Type>, - std::optional>, - std::remove_cvref_t>; + typename Underlying>::Type; }; -template -struct Underlying>> { - using Operand1Type = typename Underlying>::Type; +template +struct Underlying> { + static_assert(all_columns_exist>(), + "All columns must exist."); + using Type = remove_reflection_t< + rfl::field_type_t<_name, get_table_t, TableTupleType>>>; +}; - static_assert((true && ... && - std::is_same_v, - remove_nullable_t>::Type>>), - "All inputs into coalesce(...) must have the same type."); +template +struct Underlying>> { + using Type = remove_reflection_t< + rfl::field_type_t<_name, get_table_t, TableTupleType>>>; +}; + +template +struct Underlying>> { + using Type = std::conditional_t< + is_nullable_v>::Type>, + std::optional>, + std::remove_cvref_t>; +}; + +template +struct Underlying>> { + using Operand1Type = + typename Underlying>::Type; + + static_assert( + (true && ... && + std::is_same_v, + remove_nullable_t>::Type>>), + "All inputs into coalesce(...) must have the same type."); using Type = std::conditional_t< (is_nullable_v && ... && - is_nullable_v>::Type>), + is_nullable_v>::Type>), std::optional>, remove_nullable_t>; }; -template -struct Underlying>> { +template +struct Underlying>> { static_assert( (true && ... && - std::is_same_v>::Type>, - std::string>), + std::is_same_v< + remove_nullable_t>::Type>, + std::string>), "Must be a string"); - using Type = - std::conditional_t<(false || ... || - is_nullable_v>::Type>), - std::optional, std::string>; + using Type = std::conditional_t< + (false || ... || + is_nullable_v>::Type>), + std::optional, std::string>; }; -template -struct Underlying>> { - using Underlying1 = typename Underlying::Type; +template +struct Underlying>> { + using Underlying1 = typename Underlying::Type; static_assert(is_timestamp_v>, "Must be a timestamp"); @@ -95,11 +109,11 @@ struct Underlying; }; -template -struct Underlying< - T, Operation> { - using Underlying1 = typename Underlying::Type; - using Underlying2 = typename Underlying::Type; +template +struct Underlying> { + using Underlying1 = typename Underlying::Type; + using Underlying2 = typename Underlying::Type; static_assert(is_timestamp_v>, "Must be a timestamp"); @@ -111,15 +125,19 @@ struct Underlying< std::optional, double>; }; -template -struct Underlying< - T, Operation> { +template +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; using Underlying2 = - typename Underlying>::Type; + typename Underlying>::Type; using Underlying3 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(std::is_same_v, std::string>, "Must be a string"); @@ -134,12 +152,15 @@ struct Underlying< std::optional, std::string>; }; -template -struct Underlying> { +template +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; using Underlying2 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(std::is_integral_v> || std::is_floating_point_v>, @@ -149,9 +170,10 @@ struct Underlying> { using Type = Underlying1; }; -template -struct Underlying> { - using Underlying1 = typename Underlying::Type; +template +struct Underlying> { + using Underlying1 = typename Underlying::Type; static_assert(is_timestamp_v>, "Must be a timestamp"); @@ -160,12 +182,13 @@ struct Underlying> { std::optional, time_t>; }; -template +template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::date_part) -struct Underlying> { +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(is_timestamp_v>, "Must be a timestamp"); @@ -174,12 +197,13 @@ struct Underlying> { std::conditional_t, std::optional, int>; }; -template +template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::string) -struct Underlying> { +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(std::is_same_v, std::string>, "Must be a string"); @@ -193,14 +217,17 @@ struct Underlying> { std::conditional_t<_op == Operator::length, SizeType, StringType>; }; -template +template requires((num_operands_v<_op>) == 2 && (operator_category_v<_op>) == OperatorCategory::string) -struct Underlying> { +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; using Underlying2 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(std::is_same_v, std::string>, "Must be a string"); @@ -212,12 +239,13 @@ struct Underlying> { std::optional, std::string>; }; -template +template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) -struct Underlying> { +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert(std::is_integral_v> || std::is_floating_point_v>, @@ -226,14 +254,17 @@ struct Underlying> { using Type = Underlying1; }; -template +template requires((num_operands_v<_op>) == 2 && (operator_category_v<_op>) == OperatorCategory::numerical) -struct Underlying> { +struct Underlying> { using Underlying1 = - typename Underlying>::Type; + typename Underlying>::Type; using Underlying2 = - typename Underlying>::Type; + typename Underlying>::Type; static_assert( requires(remove_nullable_t op1, @@ -249,14 +280,14 @@ struct Underlying> { std::optional, ResultType>; }; -template -struct Underlying> { +template +struct Underlying> { using Type = remove_reflection_t<_Type>; }; -template -using underlying_t = - typename Underlying, std::remove_cvref_t>::Type; +template +using underlying_t = typename Underlying, + std::remove_cvref_t>::Type; } // namespace sqlgen::transpilation diff --git a/src/sqlgen/internal/strings/strings.cpp b/src/sqlgen/internal/strings/strings.cpp index 20890cc..2d8b4f5 100644 --- a/src/sqlgen/internal/strings/strings.cpp +++ b/src/sqlgen/internal/strings/strings.cpp @@ -10,6 +10,14 @@ char to_lower(const char ch) { } } +std::string to_lower(const std::string& _str) { + auto str = _str; + for (char& ch : str) { + ch = to_lower(ch); + } + return str; +} + char to_upper(const char ch) { if (ch >= 'a' && ch <= 'z') { return ch + ('A' - 'a'); @@ -18,6 +26,14 @@ char to_upper(const char ch) { } } +std::string to_upper(const std::string& _str) { + auto str = _str; + for (char& ch : str) { + ch = to_upper(ch); + } + return str; +} + std::string join(const std::string& _delimiter, const std::vector& _strings) { if (_strings.size() == 0) { diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 156634e..89f292b 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -6,6 +6,7 @@ #include #include +#include "sqlgen/dynamic/Join.hpp" #include "sqlgen/dynamic/Operation.hpp" #include "sqlgen/internal/collect/vector.hpp" #include "sqlgen/internal/strings/strings.hpp" @@ -41,6 +42,8 @@ std::vector get_primary_keys( std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept; +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; @@ -115,7 +118,11 @@ std::string column_or_value_to_sql( return _col.visit([&](const auto& _c) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - return wrap_in_quotes(_c.name); + if (_c.alias) { + return *_c.alias + "." + wrap_in_quotes(_c.name); + } else { + return wrap_in_quotes(_c.name); + } } else { return _c.val.visit(handle_value); } @@ -366,6 +373,35 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { return stream.str(); } +std::string join_to_sql(const dynamic::Join& _stmt) noexcept { + std::stringstream stream; + + stream << internal::strings::to_upper(internal::strings::replace_all( + rfl::enum_to_string(_stmt.how), "_", " ")) + << " "; + + stream << _stmt.table_or_query.visit( + [](const auto& _table_or_query) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return wrap_in_quotes(_table_or_query.name); + } else { + return "(" + select_from_to_sql(*_table_or_query) + ")"; + } + }) + << " "; + + stream << _stmt.alias << " "; + + if (_stmt.on) { + stream << "ON " << condition_to_sql(*_stmt.on); + } else { + stream << "ON 1 = 1"; + } + + return stream.str(); +} + std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { @@ -544,7 +580,7 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { using namespace std::ranges::views; const auto order_by_to_str = [](const auto& _w) -> std::string { - return "\"" + _w.column.name + "\"" + (_w.desc ? " DESC" : ""); + return column_or_value_to_sql(_w.column) + (_w.desc ? " DESC" : ""); }; std::stringstream stream; @@ -559,6 +595,17 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { } stream << wrap_in_quotes(_stmt.table.name); + if (_stmt.alias) { + stream << " " << *_stmt.alias; + } + + if (_stmt.joins) { + stream << " " + << internal::strings::join( + " ", internal::collect::vector(*_stmt.joins | + transform(join_to_sql))); + } + if (_stmt.where) { stream << " WHERE " << condition_to_sql(*_stmt.where); } @@ -582,8 +629,6 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } - stream << ";"; - return stream.str(); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 3a76fca..5b34dea 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -2,6 +2,7 @@ #include #include +#include "sqlgen/dynamic/Join.hpp" #include "sqlgen/dynamic/Operation.hpp" #include "sqlgen/internal/collect/vector.hpp" #include "sqlgen/internal/strings/strings.hpp" @@ -37,6 +38,8 @@ std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept; template std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept; +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; @@ -47,6 +50,16 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept; std::string update_to_sql(const dynamic::Update& _stmt) noexcept; +// ---------------------------------------------------------------------------- + +inline std::string get_name(const dynamic::Column& _col) { return _col.name; } + +inline std::string wrap_in_quotes(const std::string& _name) noexcept { + return "\"" + _name + "\""; +} + +// ---------------------------------------------------------------------------- + std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { @@ -127,7 +140,11 @@ std::string column_or_value_to_sql( return _col.visit([&](const auto& _c) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - return "\"" + _c.name + "\""; + if (_c.alias) { + return *_c.alias + "." + wrap_in_quotes(_c.name); + } else { + return wrap_in_quotes(_c.name); + } } else { return _c.val.visit(handle_value); } @@ -360,6 +377,35 @@ std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept { return stream.str(); } +std::string join_to_sql(const dynamic::Join& _stmt) noexcept { + std::stringstream stream; + + stream << internal::strings::to_upper(internal::strings::replace_all( + rfl::enum_to_string(_stmt.how), "_", " ")) + << " "; + + stream << _stmt.table_or_query.visit( + [](const auto& _table_or_query) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return wrap_in_quotes(_table_or_query.name); + } else { + return "(" + select_from_to_sql(*_table_or_query) + ")"; + } + }) + << " "; + + stream << _stmt.alias << " "; + + if (_stmt.on) { + stream << "ON " << condition_to_sql(*_stmt.on); + } else { + stream << "ON 1 = 1"; + } + + return stream.str(); +} + std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { @@ -542,7 +588,7 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { using namespace std::ranges::views; const auto order_by_to_str = [](const auto& _w) -> std::string { - return "\"" + _w.column.name + "\"" + (_w.desc ? " DESC" : ""); + return column_or_value_to_sql(_w.column) + (_w.desc ? " DESC" : ""); }; std::stringstream stream; @@ -553,9 +599,20 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " FROM "; if (_stmt.table.schema) { - stream << "\"" << *_stmt.table.schema << "\"."; + stream << wrap_in_quotes(*_stmt.table.schema) << "."; + } + stream << wrap_in_quotes(_stmt.table.name); + + if (_stmt.alias) { + stream << " " << *_stmt.alias; + } + + if (_stmt.joins) { + stream << " " + << internal::strings::join( + " ", internal::collect::vector(*_stmt.joins | + transform(join_to_sql))); } - stream << "\"" << _stmt.table.name << "\""; if (_stmt.where) { stream << " WHERE " << condition_to_sql(*_stmt.where); @@ -580,8 +637,6 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } - stream << ";"; - return stream.str(); } diff --git a/tests/postgres/test_is_null_dry.cpp b/tests/postgres/test_is_null_dry.cpp index 7acc501..d015458 100644 --- a/tests/postgres/test_is_null_dry.cpp +++ b/tests/postgres/test_is_null_dry.cpp @@ -23,7 +23,7 @@ TEST(postgres, test_is_null_dry) { order_by("first_name"_c.desc())); const std::string expected = - R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "age" IS NULL ORDER BY "first_name" DESC;)"; + R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "age" IS NULL ORDER BY "first_name" DESC)"; EXPECT_EQ(sql, expected); } diff --git a/tests/postgres/test_join.cpp b/tests/postgres/test_join.cpp new file mode 100644 index 0000000..116d37d --- /dev/null +++ b/tests/postgres/test_join.cpp @@ -0,0 +1,64 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_join { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +TEST(postgres, test_join) { + static_assert(std::ranges::input_range>, + "Must be an input range."); + + const 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 credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto get_people = + select_from( + "id"_t1 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t2 | as<"last_name">, "age"_t2 | as<"age">) | + left_join("id"_t1 == "id"_t2) | order_by("id"_t1) | + to>; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."id" AS "id", t1."first_name" AS "first_name", t2."last_name" AS "last_name", t2."age" AS "age" FROM "Person" t1 LEFT JOIN "Person" t2 ON t1."id" = t2."id" ORDER BY t1."id")"; + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45.0},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10.0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8.0},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0.0}])"; + + EXPECT_EQ(postgres::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_join + +#endif diff --git a/tests/postgres/test_joins_nested.cpp b/tests/postgres/test_joins_nested.cpp new file mode 100644 index 0000000..0182353 --- /dev/null +++ b/tests/postgres/test_joins_nested.cpp @@ -0,0 +1,94 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_nested { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + sqlgen::PrimaryKey parent_id; + sqlgen::PrimaryKey child_id; +}; + +TEST(postgres, test_joins_nested) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_children = + select_from("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + left_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t1 | as<"first_name_parent">, + "first_name"_t2 | as<"first_name_child">, + ("age"_t1 - "age"_t2) | as<"parent_age_at_birth">) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + order_by("id"_t1, "id"_t2) | to>; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t1."first_name" AS "first_name_parent", t2."first_name" AS "first_name_child", (t1."age") - (t2."age") AS "parent_age_at_birth" FROM "Person" t1 INNER JOIN (SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" FROM "Relationship" t1 LEFT JOIN "Person" t2 ON t2."id" = t1."child_id") t2 ON t1."id" = t2."id" ORDER BY t1."id", t2."id")"; + + const std::string expected = + R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(postgres::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested + +#endif diff --git a/tests/postgres/test_joins_nested_grouped.cpp b/tests/postgres/test_joins_nested_grouped.cpp new file mode 100644 index 0000000..71355a8 --- /dev/null +++ b/tests/postgres/test_joins_nested_grouped.cpp @@ -0,0 +1,93 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_nested_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + sqlgen::PrimaryKey parent_id; + sqlgen::PrimaryKey child_id; +}; + +TEST(postgres, test_joins_nested) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_child; + double avg_parent_age_at_birth; + }; + + const auto get_children = + select_from("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + left_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t2 | as<"first_name_child">, + avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth">) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + group_by("last_name"_t1, "first_name"_t2) | order_by("first_name"_t2) | + to>; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t2."first_name" AS "first_name_child", AVG((t1."age") - (t2."age")) AS "avg_parent_age_at_birth" FROM "Person" t1 INNER JOIN (SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" FROM "Relationship" t1 LEFT JOIN "Person" t2 ON t2."id" = t1."child_id") t2 ON t1."id" = t2."id" GROUP BY t1."last_name", t2."first_name" ORDER BY t2."first_name")"; + + const std::string expected = + R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])"; + + EXPECT_EQ(postgres::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested_grouped + +#endif diff --git a/tests/postgres/test_joins_two_tables.cpp b/tests/postgres/test_joins_two_tables.cpp new file mode 100644 index 0000000..7e5b917 --- /dev/null +++ b/tests/postgres/test_joins_two_tables.cpp @@ -0,0 +1,88 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_two_tables { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + sqlgen::PrimaryKey parent_id; + sqlgen::PrimaryKey child_id; +}; + +TEST(postgres, test_joins_two_tables) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t1 | as<"first_name_parent">, + "first_name"_t3 | as<"first_name_child">, + ("age"_t1 - "age"_t3) | as<"parent_age_at_birth">) | + inner_join("id"_t1 == "parent_id"_t2) | + left_join("id"_t3 == "child_id"_t2) | + order_by("id"_t1, "id"_t3) | to>; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t1."first_name" AS "first_name_parent", t3."first_name" AS "first_name_child", (t1."age") - (t3."age") AS "parent_age_at_birth" FROM "Person" t1 INNER JOIN "Relationship" t2 ON t1."id" = t2."parent_id" LEFT JOIN "Person" t3 ON t3."id" = t2."child_id" ORDER BY t1."id", t3."id")"; + const std::string expected = + R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(postgres::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables + +#endif diff --git a/tests/postgres/test_joins_two_tables_grouped.cpp b/tests/postgres/test_joins_two_tables_grouped.cpp new file mode 100644 index 0000000..90c8eec --- /dev/null +++ b/tests/postgres/test_joins_two_tables_grouped.cpp @@ -0,0 +1,89 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_two_tables_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + sqlgen::PrimaryKey parent_id; + sqlgen::PrimaryKey child_id; +}; + +TEST(postgres, test_joins_two_tables_grouped) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_child; + double avg_parent_age_at_birth; + }; + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t3 | as<"first_name_child">, + avg("age"_t1 - "age"_t3) | as<"avg_parent_age_at_birth">) | + inner_join("id"_t1 == "parent_id"_t2) | + left_join("id"_t3 == "child_id"_t2) | + group_by("last_name"_t1, "first_name"_t3) | + order_by("last_name"_t1, "first_name"_t3) | + to>; + + const auto people = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t3."first_name" AS "first_name_child", AVG((t1."age") - (t3."age")) AS "avg_parent_age_at_birth" FROM "Person" t1 INNER JOIN "Relationship" t2 ON t1."id" = t2."parent_id" LEFT JOIN "Person" t3 ON t3."id" = t2."child_id" GROUP BY t1."last_name", t3."first_name" ORDER BY t1."last_name", t3."first_name")"; + + const std::string expected = + R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])"; + + EXPECT_EQ(postgres::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables_grouped + +#endif diff --git a/tests/postgres/test_like_dry.cpp b/tests/postgres/test_like_dry.cpp index c43aa3c..6547d2e 100644 --- a/tests/postgres/test_like_dry.cpp +++ b/tests/postgres/test_like_dry.cpp @@ -23,7 +23,7 @@ TEST(postgres, test_like_dry) { where("first_name"_c.like("H%")) | order_by("age"_c)); const std::string expected = - R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "first_name" LIKE 'H%' ORDER BY "age";)"; + R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "first_name" LIKE 'H%' ORDER BY "age")"; EXPECT_EQ(sql, expected); } diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp index c0e168c..56a7f44 100644 --- a/tests/postgres/test_select_from_with_timestamps.cpp +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -75,7 +75,7 @@ TEST(postgres, test_range_select_from_with_timestamps) { .value(); const std::string expected_query = - R"(SELECT "birthday" + INTERVAL '10 days' AS "birthday", cast((cast(extract(YEAR from "birthday") as TEXT) || '-' || cast(extract(MONTH from "birthday") as TEXT) || '-' || cast(extract(DAY from "birthday") as TEXT)) as TIMESTAMP) + INTERVAL '10 days' AS "birthday_recreated", cast('2011-01-01' as DATE) - cast("birthday" as DATE) AS "age_in_days", extract(EPOCH FROM "birthday" + INTERVAL '10 days') AS "birthday_unixepoch", extract(HOUR from "birthday") AS "hour", extract(MINUTE from "birthday") AS "minute", extract(SECOND from "birthday") AS "second", extract(DOW from "birthday") AS "weekday" FROM "Person" ORDER BY "id";)"; + R"(SELECT "birthday" + INTERVAL '10 days' AS "birthday", cast((cast(extract(YEAR from "birthday") as TEXT) || '-' || cast(extract(MONTH from "birthday") as TEXT) || '-' || cast(extract(DAY from "birthday") as TEXT)) as TIMESTAMP) + INTERVAL '10 days' AS "birthday_recreated", cast('2011-01-01' as DATE) - cast("birthday" as DATE) AS "age_in_days", extract(EPOCH FROM "birthday" + INTERVAL '10 days') AS "birthday_unixepoch", extract(HOUR from "birthday") AS "hour", extract(MINUTE from "birthday") AS "minute", extract(SECOND from "birthday") AS "second", extract(DOW from "birthday") AS "weekday" FROM "Person" ORDER BY "id")"; const std::string expected = R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-11","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])"; diff --git a/tests/postgres/test_to_select_from2_dry.cpp b/tests/postgres/test_to_select_from2_dry.cpp index 3929525..bb7e547 100644 --- a/tests/postgres/test_to_select_from2_dry.cpp +++ b/tests/postgres/test_to_select_from2_dry.cpp @@ -24,7 +24,7 @@ TEST(postgres, test_to_select_from2_dry) { order_by("field1"_c) | limit(10); const auto expected = - R"(SELECT "field1" AS "field", AVG("field2") AS "avg_field2", "nullable" AS "nullable_field", 1 AS "one", 'hello' AS "hello" FROM "TestTable" WHERE "id" > 0 GROUP BY "field1", "nullable" ORDER BY "field1" LIMIT 10;)"; + R"(SELECT "field1" AS "field", AVG("field2") AS "avg_field2", "nullable" AS "nullable_field", 1 AS "one", 'hello' AS "hello" FROM "TestTable" WHERE "id" > 0 GROUP BY "field1", "nullable" ORDER BY "field1" LIMIT 10)"; EXPECT_EQ(sqlgen::postgres::to_sql(query), expected); } diff --git a/tests/postgres/test_to_select_from_dry.cpp b/tests/postgres/test_to_select_from_dry.cpp index 2aeec5d..7559b8e 100644 --- a/tests/postgres/test_to_select_from_dry.cpp +++ b/tests/postgres/test_to_select_from_dry.cpp @@ -16,7 +16,7 @@ TEST(postgres, test_to_select_from_dry) { const auto query = sqlgen::read>; const auto expected = - R"(SELECT "field1", "field2", "id", "nullable" FROM "TestTable";)"; + R"(SELECT "field1", "field2", "id", "nullable" FROM "TestTable")"; EXPECT_EQ(sqlgen::postgres::to_sql(query), expected); } diff --git a/tests/postgres/test_where_dry.cpp b/tests/postgres/test_where_dry.cpp index 284099d..6991da7 100644 --- a/tests/postgres/test_where_dry.cpp +++ b/tests/postgres/test_where_dry.cpp @@ -21,7 +21,7 @@ TEST(postgres, test_where_dry) { order_by("age"_c); const auto expected = - R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE ("age" < 18) AND ("first_name" != 'Hugo') ORDER BY "age";)"; + R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE ("age" < 18) AND ("first_name" != 'Hugo') ORDER BY "age")"; EXPECT_EQ(sqlgen::postgres::to_sql(query), expected); } diff --git a/tests/sqlite/test_join.cpp b/tests/sqlite/test_join.cpp new file mode 100644 index 0000000..77263af --- /dev/null +++ b/tests/sqlite/test_join.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_join { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +TEST(sqlite, test_join) { + const 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}}); + + using namespace sqlgen; + + const auto get_people = + select_from( + "id"_t1 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t2 | as<"last_name">, "age"_t2 | as<"age">) | + left_join("id"_t1 == "id"_t2) | order_by("id"_t1) | + to>; + + const auto people = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."id" AS "id", t1."first_name" AS "first_name", t2."last_name" AS "last_name", t2."age" AS "age" FROM "Person" t1 LEFT JOIN "Person" t2 ON t1."id" = t2."id" ORDER BY t1."id")"; + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45.0},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10.0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8.0},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0.0}])"; + + EXPECT_EQ(sqlite::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_join diff --git a/tests/sqlite/test_joins_nested.cpp b/tests/sqlite/test_joins_nested.cpp new file mode 100644 index 0000000..d794780 --- /dev/null +++ b/tests/sqlite/test_joins_nested.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_nested { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(sqlite, test_joins_nested) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_children = + select_from("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + left_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t1 | as<"first_name_parent">, + "first_name"_t2 | as<"first_name_child">, + ("age"_t1 - "age"_t2) | as<"parent_age_at_birth">) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + order_by("id"_t1, "id"_t2) | to>; + + const auto people = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t1."first_name" AS "first_name_parent", t2."first_name" AS "first_name_child", (t1."age") - (t2."age") AS "parent_age_at_birth" FROM "Person" t1 INNER JOIN (SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" FROM "Relationship" t1 LEFT JOIN "Person" t2 ON t2."id" = t1."child_id") t2 ON t1."id" = t2."id" ORDER BY t1."id", t2."id")"; + const std::string expected = + R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(sqlite::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested diff --git a/tests/sqlite/test_joins_nested_grouped.cpp b/tests/sqlite/test_joins_nested_grouped.cpp new file mode 100644 index 0000000..8f74fbd --- /dev/null +++ b/tests/sqlite/test_joins_nested_grouped.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_nested_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(sqlite, test_joins_nested_grouped) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_child; + double avg_parent_age_at_birth; + }; + + const auto get_children = + select_from("parent_id"_t1 | as<"id">, + "first_name"_t2 | as<"first_name">, + "age"_t2 | as<"age">) | + left_join("id"_t2 == "child_id"_t1); + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t2 | as<"first_name_child">, + avg("age"_t1 - "age"_t2) | as<"avg_parent_age_at_birth">) | + inner_join<"t2">(get_children, "id"_t1 == "id"_t2) | + group_by("last_name"_t1, "first_name"_t2) | order_by("first_name"_t2) | + to>; + + const auto people = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t2."first_name" AS "first_name_child", AVG((t1."age") - (t2."age")) AS "avg_parent_age_at_birth" FROM "Person" t1 INNER JOIN (SELECT t1."parent_id" AS "id", t2."first_name" AS "first_name", t2."age" AS "age" FROM "Relationship" t1 LEFT JOIN "Person" t2 ON t2."id" = t1."child_id") t2 ON t1."id" = t2."id" GROUP BY t1."last_name", t2."first_name" ORDER BY t2."first_name")"; + + const std::string expected = + R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])"; + EXPECT_EQ(sqlite::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_nested_grouped diff --git a/tests/sqlite/test_joins_two_tables.cpp b/tests/sqlite/test_joins_two_tables.cpp new file mode 100644 index 0000000..678a59c --- /dev/null +++ b/tests/sqlite/test_joins_two_tables.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_two_tables { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(sqlite, test_joins_two_tables) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t1 | as<"first_name_parent">, + "first_name"_t3 | as<"first_name_child">, + ("age"_t1 - "age"_t3) | as<"parent_age_at_birth">) | + inner_join("id"_t1 == "parent_id"_t2) | + left_join("id"_t3 == "child_id"_t2) | + order_by("id"_t1, "id"_t3) | to>; + + const auto people = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t1."first_name" AS "first_name_parent", t3."first_name" AS "first_name_child", (t1."age") - (t3."age") AS "parent_age_at_birth" FROM "Person" t1 INNER JOIN "Relationship" t2 ON t1."id" = t2."parent_id" LEFT JOIN "Person" t3 ON t3."id" = t2."child_id" ORDER BY t1."id", t3."id")"; + const std::string expected = + R"([{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Bart","parent_age_at_birth":35.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Maggie","parent_age_at_birth":45.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(sqlite::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables diff --git a/tests/sqlite/test_joins_two_tables_grouped.cpp b/tests/sqlite/test_joins_two_tables_grouped.cpp new file mode 100644 index 0000000..2dd836b --- /dev/null +++ b/tests/sqlite/test_joins_two_tables_grouped.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_joins_two_tables_grouped { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +struct Relationship { + uint32_t parent_id; + uint32_t child_id; +}; + +TEST(sqlite, test_joins_two_tables_grouped) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{ + .id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40}, + Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 4, .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; + + struct ParentAndChild { + std::string last_name; + std::string first_name_child; + double avg_parent_age_at_birth; + }; + + const auto get_people = + select_from( + "last_name"_t1 | as<"last_name">, + "first_name"_t3 | as<"first_name_child">, + avg("age"_t1 - "age"_t3) | as<"avg_parent_age_at_birth">) | + inner_join("id"_t1 == "parent_id"_t2) | + left_join("id"_t3 == "child_id"_t2) | + group_by("last_name"_t1, "first_name"_t3) | + order_by("last_name"_t1, "first_name"_t3) | + to>; + + const auto people = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(write(std::ref(relationships))) + .and_then(get_people) + .value(); + + const std::string expected_query = + R"(SELECT t1."last_name" AS "last_name", t3."first_name" AS "first_name_child", AVG((t1."age") - (t3."age")) AS "avg_parent_age_at_birth" FROM "Person" t1 INNER JOIN "Relationship" t2 ON t1."id" = t2."parent_id" LEFT JOIN "Person" t3 ON t3."id" = t2."child_id" GROUP BY t1."last_name", t3."first_name" ORDER BY t1."last_name", t3."first_name")"; + + const std::string expected = + R"([{"last_name":"Simpson","first_name_child":"Bart","avg_parent_age_at_birth":32.5},{"last_name":"Simpson","first_name_child":"Lisa","avg_parent_age_at_birth":34.5},{"last_name":"Simpson","first_name_child":"Maggie","avg_parent_age_at_birth":42.5}])"; + + EXPECT_EQ(sqlite::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_two_tables_grouped diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp index c1d92cb..76a3bcf 100644 --- a/tests/sqlite/test_select_from_with_timestamps.cpp +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -66,7 +66,7 @@ TEST(sqlite, test_range_select_from_with_timestamps) { .value(); const std::string expected_query = - R"(SELECT datetime("birthday", '+10 days') AS "birthday", cast((cast(cast(strftime('%Y', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%m', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%d', "birthday") as INT) as TEXT)) as TEXT) AS "birthday_recreated", julianday('2011-01-01') - julianday("birthday") AS "age_in_days", unixepoch(datetime("birthday", '+10 days'), 'subsec') AS "birthday_unixepoch", cast(strftime('%H', "birthday") as INT) AS "hour", cast(strftime('%M', "birthday") as INT) AS "minute", cast(strftime('%S', "birthday") as INT) AS "second", cast(strftime('%w', "birthday") as INT) AS "weekday" FROM "Person" ORDER BY "id";)"; + R"(SELECT datetime("birthday", '+10 days') AS "birthday", cast((cast(cast(strftime('%Y', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%m', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%d', "birthday") as INT) as TEXT)) as TEXT) AS "birthday_recreated", julianday('2011-01-01') - julianday("birthday") AS "age_in_days", unixepoch(datetime("birthday", '+10 days'), 'subsec') AS "birthday_unixepoch", cast(strftime('%H', "birthday") as INT) AS "hour", cast(strftime('%M', "birthday") as INT) AS "minute", cast(strftime('%S', "birthday") as INT) AS "second", cast(strftime('%w', "birthday") as INT) AS "weekday" FROM "Person" ORDER BY "id")"; const std::string expected = R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-01","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-01","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-01","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-01","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])"; diff --git a/tests/sqlite/test_to_select_from.cpp b/tests/sqlite/test_to_select_from.cpp index 598072e..389ad80 100644 --- a/tests/sqlite/test_to_select_from.cpp +++ b/tests/sqlite/test_to_select_from.cpp @@ -17,7 +17,7 @@ TEST(sqlite, test_to_select_from) { sqlgen::transpilation::read_to_select_from(); const auto conn = sqlgen::sqlite::connect().value(); const auto expected = - R"(SELECT "field1", "field2", "id", "nullable" FROM "TestTable";)"; + R"(SELECT "field1", "field2", "id", "nullable" FROM "TestTable")"; EXPECT_EQ(conn->to_sql(select_from_stmt), expected); }