diff --git a/README.md b/README.md index 702a838..7cf4dd5 100644 --- a/README.md +++ b/README.md @@ -336,19 +336,23 @@ struct ParentAndChild { double parent_age_at_birth; }; +// First, create a subquery 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); +// Then use it as a source for another query 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) | + inner_join<"t2">( + get_children, // Use the subquery as the source + "id"_t1 == "id"_t2) | order_by("id"_t1, "id"_t2) | to>; ``` @@ -371,6 +375,57 @@ ON t1."id" = t2."id" ORDER BY t1."id", t2."id" ``` +Or: +```cpp +using namespace sqlgen; +using namespace sqlgen::literals; + +struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; +}; + +// First, create a subquery +const auto get_parents = select_from( + "child_id"_t2 | as<"id">, + "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, + "age"_t1 | as<"age"> +) | left_join("id"_t1 == "parent_id"_t2); + +// Then use it as a source for another query +const auto get_people = select_from<"t1">( + get_parents, // Use the subquery as the source + "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("id"_t1 == "id"_t2) | + order_by("id"_t1, "id"_t2) | to>; +``` + +Generated SQL: +```sql +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 ( + SELECT t2."child_id" AS "id", + t1."first_name" AS "first_name", + t1."last_name" AS "last_name", + t1."age" AS "age" + FROM "Person" t1 + LEFT JOIN "Relationship" t2 + ON t1."id" = t2."parent_id" +) t1 +INNER JOIN "Person" t2 +ON t1."id" = t2."id" +ORDER BY t1."id", t2."id" +``` + ### Type Safety and SQL Injection Protection sqlgen provides comprehensive compile-time checks and runtime protection: diff --git a/docs/README.md b/docs/README.md index 1b86101..bd6464c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::group_by and Aggregations](group_by_and_aggregations.md) - How generate GROUP BY queries and aggregate data - [sqlgen::inner_join, sqlgen::left_join, sqlgen::right_join, sqlgen::full_join](joins.md) - How to join different tables - [sqlgen::insert](insert.md) - How to insert data within transactions +- [sqlgen::select_from](select_from.md) - How to read data from a database using more complex queries - [sqlgen::update](update.md) - How to update data in a table ## Other Operations diff --git a/docs/select_from.md b/docs/select_from.md new file mode 100644 index 0000000..682e322 --- /dev/null +++ b/docs/select_from.md @@ -0,0 +1,305 @@ +# `sqlgen::select_from` + +The `sqlgen::select_from` function provides a type-safe, composable interface for building SQL SELECT queries in C++. It supports selecting from tables, subqueries, and complex nested queries with full support for joins, filtering, grouping, ordering, and result type conversion. + +## Basic Usage + +### Selecting from a Table + +```cpp +using namespace sqlgen; +using namespace sqlgen::literals; + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +// Select specific columns +const auto query = select_from("first_name"_c, "last_name"_c, "age"_c); + +// Execute the query +const auto people = sqlite::connect() + .and_then(query | to>) + .value(); +``` + +This generates: +```sql +SELECT "first_name", "last_name", "age" FROM "Person" +``` + +### Selecting with Aliases + +Use aliases to rename columns in the result set: + +```cpp +struct Result { + std::string name; + int person_age; +}; + +const auto query = select_from( + "first_name"_c | as<"name">, + "age"_c | as<"person_age"> +) | to>; +``` + +This generates: +```sql +SELECT "first_name" AS "name", "age" AS "person_age" FROM "Person" +``` + +## Table Aliases + +### Aliased Tables + +When you need to reference the same table multiple times or want cleaner column references: + +```cpp +const auto query = select_from( + "first_name"_t1, + "last_name"_t1, + "age"_t1 +) | where("age"_t1 > 18); +``` + +This generates: +```sql +SELECT t1."first_name", t1."last_name", t1."age" FROM "Person" t1 WHERE t1."age" > 18 +``` + +## Selecting from Subqueries + +You can use the result of one query as the source for another query: + +```cpp +// First, create a subquery +const auto get_parents = select_from( + "child_id"_t2 | as<"id">, + "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, + "age"_t1 | as<"age"> +) | left_join("id"_t1 == "parent_id"_t2); + +// Then use it as a source for another query +const auto get_people = select_from<"t1">( + get_parents, // Use the subquery as the source + "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("id"_t1 == "id"_t2); +``` + +This generates: +```sql +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 ( + SELECT t2."child_id" AS "id", t1."first_name" AS "first_name", t1."last_name" AS "last_name", t1."age" AS "age" + FROM "Person" t1 + LEFT JOIN "Relationship" t2 ON t1."id" = t2."parent_id" +) t1 +INNER JOIN "Person" t2 ON t1."id" = t2."id" +``` + +## Expressions and Calculations + +You can select calculated expressions and literal values: + +```cpp +struct Result { + std::string field; + double avg_field2; + std::optional nullable_field; + int one; + std::string hello; +}; + +const auto query = select_from( + "field1"_c.as<"field">(), + avg("field2"_c).as<"avg_field2">(), + "nullable"_c | as<"nullable_field">, + as<"one">(1), // Literal value + "hello" | as<"hello"> // String literal +) | to; +``` + +## Query Composition + +`select_from` can be combined with various query operations: + +```cpp +const auto query = select_from( + "first_name"_c, + "last_name"_c, + "age"_c +) +| where("age"_c >= 18) // Filter results +| order_by("last_name"_c, "first_name"_c) // Order results +| limit(10) // Limit number of results +| to>; // Convert to container +``` + +### With Joins + +```cpp +const auto query = 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>; +``` + +## Result Type Conversion + +### Using `to` + +Convert query results to specific container types: + +```cpp +// Convert to vector +const auto people_vec = query | to>; + +// Convert to single result (query must return exactly one row) +const auto person = query | to; + +// Return as Range (lazy evaluation) +const auto people_range = query; // Returns Range by default +``` + +### Automatic Type Deduction + +If you don't specify `to<...>`, `select_from` returns a `Range` type that can be iterated: + +```cpp +const auto query = select_from("first_name"_c, "age"_c); + +// Execute and iterate +for (const auto& result : sqlite::connect().and_then(query).value()) { + if (result) { + std::cout << result->first_name << ": " << result->age << std::endl; + } +} +``` + +## Syntax Variations + +### Three Main Forms + +1. **Table without alias:** + ```cpp + select_from(fields...) + ``` + +2. **Table with alias:** + ```cpp + select_from(fields...) + ``` + +3. **Subquery with alias:** + ```cpp + select_from<"t1">(subquery, fields...) + ``` + +## Column References + +Use the predefined column literals for clean syntax: + +```cpp +using namespace sqlgen::literals; + +// Use _c for unaliased tables +"column_name"_c + +// Use _t1, _t2, etc. for aliased tables/joins +"column_name"_t1, "column_name"_t2 + +// Or use explicit col syntax +col<"column_name", "t1"> +``` + +## Advanced Examples + +### Complex Nested Query + +```cpp +struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; +}; + +// Step 1: Create a subquery to get parent-child relationships +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); + +// Step 2: Use the subquery in a larger query +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>; +``` + +### Aggregation with Grouping + +```cpp +const auto summary = select_from( + "last_name"_c, + count() | as<"family_size">, + avg("age"_c) | as<"avg_age">, + min("age"_c) | as<"youngest">, + max("age"_c) | as<"oldest"> +) +| group_by("last_name"_c) +| order_by("last_name"_c) +| to>; +``` + +## Important Notes + +- **Field Matching**: Result struct field names must exactly match the aliases used in the query +- **Type Safety**: All column references and types are checked at compile time +- **Null Handling**: Use `std::optional` for nullable columns in result types +- **Execution**: Queries are executed when passed to a database connection +- **Composition**: Queries can be built incrementally and reused as subqueries +- **Aliases Required**: When using joins or subqueries, table aliases are mandatory for disambiguation and must follow the pattern `t1`, `t2`, `t3`, etc. + +## Error Handling + +Queries return `Result` types that must be handled: + +```cpp +const auto result = sqlite::connect() + .and_then(query) + .value(); // Throws on error + +// Or handle errors explicitly +const auto result = sqlite::connect() + .and_then(query); + +if (result) { + // Success + use_data(*result); +} else { + // Handle error + std::cerr << "Query failed: " << result.error().what() << std::endl; +} +``` + diff --git a/include/sqlgen/dynamic/SelectFrom.hpp b/include/sqlgen/dynamic/SelectFrom.hpp index 7ec8537..c0ee22a 100644 --- a/include/sqlgen/dynamic/SelectFrom.hpp +++ b/include/sqlgen/dynamic/SelectFrom.hpp @@ -18,6 +18,8 @@ namespace sqlgen::dynamic { struct SelectFrom { + using TableOrQueryType = rfl::Variant>; + struct Field { Operation val; std::optional as; @@ -25,12 +27,12 @@ struct SelectFrom { struct Join { JoinType how; - rfl::Variant> table_or_query; + TableOrQueryType table_or_query; std::string alias; std::optional on; }; - Table table; + TableOrQueryType table_or_query; std::vector fields; std::optional alias = std::nullopt; std::optional> joins = std::nullopt; diff --git a/include/sqlgen/select_from.hpp b/include/sqlgen/select_from.hpp index 996508a..68efd01 100644 --- a/include/sqlgen/select_from.hpp +++ b/include/sqlgen/select_from.hpp @@ -19,6 +19,7 @@ #include "order_by.hpp" #include "to.hpp" #include "transpilation/Join.hpp" +#include "transpilation/TableWrapper.hpp" #include "transpilation/fields_to_named_tuple_t.hpp" #include "transpilation/group_by_t.hpp" #include "transpilation/order_by_t.hpp" @@ -31,19 +32,20 @@ namespace sqlgen { template + class TableOrQueryType, class JoinsType, class WhereType, + class GroupByType, class OrderByType, class LimitType, + class ContainerType, class Connection> requires is_connection auto select_from_impl(const Ref& _conn, const FieldsType& _fields, + const TableOrQueryType& _table_or_query, const JoinsType& _joins, const WhereType& _where, const LimitType& _limit) { if constexpr (internal::is_range_v) { const auto query = transpilation::to_select_from(_fields, _joins, - _where, _limit); + TableOrQueryType, JoinsType, WhereType, + GroupByType, OrderByType, LimitType>( + _fields, _table_or_query, _joins, _where, _limit); return _conn->read(query).transform( [](auto&& _it) { return ContainerType(_it); }); @@ -65,36 +67,40 @@ auto select_from_impl(const Ref& _conn, const FieldsType& _fields, using RangeType = Range< transpilation::fields_to_named_tuple_t>; - return select_from_impl(_conn, _fields, _joins, _where, _limit) + return select_from_impl( + _conn, _fields, _table_or_query, _joins, _where, _limit) .and_then(to_container); } } template + class TableOrQueryType, class JoinsType, class WhereType, + class GroupByType, class OrderByType, class LimitType, + class ContainerType, class Connection> requires is_connection auto select_from_impl(const Result>& _res, - const FieldsType& _fields, const JoinsType& _joins, - const WhereType& _where, const LimitType& _limit) { + const FieldsType& _fields, + const TableOrQueryType& _table_or_query, + const JoinsType& _joins, const WhereType& _where, + const LimitType& _limit) { return _res.and_then([&](const auto& _conn) { - return select_from_impl(_conn, _joins, _where, _limit); + return select_from_impl( + _conn, _table_or_query, _joins, _where, _limit); }); } -template struct SelectFrom { auto operator()(const auto& _conn) const { using TableTupleType = - transpilation::table_tuple_t; + transpilation::table_tuple_t; if constexpr (std::is_same_v || std::ranges::input_range>) { @@ -103,10 +109,10 @@ struct SelectFrom { Range>, ToType>; - return select_from_impl(_conn, fields_, joins_, where_, - limit_); + return select_from_impl< + TableTupleType, AliasType, FieldsType, TableOrQueryType, JoinsType, + WhereType, GroupByType, OrderByType, LimitType, ContainerType>( + _conn, fields_, from_, joins_, where_, limit_); } else { const auto extract_result = [](auto&& _vec) -> Result { @@ -119,19 +125,20 @@ struct SelectFrom { return std::move(_vec[0]); }; - return select_from_impl>>( - _conn, fields_, joins_, where_, limit_) + _conn, fields_, from_, joins_, where_, limit_) .and_then(extract_result); } } - template friend auto operator|( const SelectFrom& _s, - const transpilation::Join& + const transpilation::Join& _join) { static_assert(std::is_same_v, "You cannot call where(...) before a join."); @@ -146,23 +153,25 @@ struct SelectFrom { if constexpr (std::is_same_v) { using NewJoinsType = rfl::Tuple< - transpilation::Join>; + transpilation::Join>; - return SelectFrom{ - .fields_ = _s.fields_, .joins_ = NewJoinsType(_join)}; + .fields_ = _s.fields_, + .from_ = _s.from_, + .joins_ = NewJoinsType(_join)}; } else { using TupleType = rfl::Tuple< - transpilation::Join>; + 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}; + .fields_ = _s.fields_, .from_ = _s.from_, .joins_ = joins}; } } @@ -180,10 +189,12 @@ 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_, .joins_ = _s.joins_, .where_ = _where.condition}; + ToType>{.fields_ = _s.fields_, + .from_ = _s.from_, + .joins_ = _s.joins_, + .where_ = _where.condition}; } template @@ -200,11 +211,14 @@ struct SelectFrom { "You cannot call to<...> before group_by(...)."); static_assert(sizeof...(ColTypes) != 0, "You must assign at least one column to group_by."); - return SelectFrom< - StructType, AliasType, FieldsType, JoinsType, WhereType, - transpilation::group_by_t, - OrderByType, LimitType, ToType>{ - .fields_ = _s.fields_, .joins_ = _s.joins_, .where_ = _s.where_}; + return SelectFrom, + OrderByType, LimitType, ToType>{.fields_ = _s.fields_, + .from_ = _s.from_, + .joins_ = _s.joins_, + .where_ = _s.where_}; } template @@ -221,23 +235,27 @@ struct SelectFrom { "You must assign at least one column to order_by."); using TableTupleType = - transpilation::table_tuple_t; + transpilation::table_tuple_t; using NewOrderByType = transpilation::order_by_t< TableTupleType, GroupByType, typename std::remove_cvref_t::ColType...>; - return SelectFrom{ - .fields_ = _s.fields_, .joins_ = _s.joins_, .where_ = _s.where_}; + return SelectFrom{.fields_ = _s.fields_, + .from_ = _s.from_, + .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{ + return SelectFrom{ .fields_ = _s.fields_, + .from_ = _s.from_, .joins_ = _s.joins_, .where_ = _s.where_, .limit_ = _limit}; @@ -247,16 +265,19 @@ struct SelectFrom { friend auto operator|(const SelectFrom& _s, const To&) { static_assert(std::is_same_v, "You cannot call to<...> twice."); - return SelectFrom{ - .fields_ = _s.fields_, - .joins_ = _s.joins_, - .where_ = _s.where_, - .limit_ = _s.limit_}; + return SelectFrom{.fields_ = _s.fields_, + .from_ = _s.from_, + .joins_ = _s.joins_, + .where_ = _s.where_, + .limit_ = _s.limit_}; } FieldsType fields_; + TableOrQueryType from_; + JoinsType joins_; WhereType where_; @@ -266,62 +287,66 @@ struct SelectFrom { namespace transpilation { -template +template struct ExtractTable< - SelectFrom> { - using TableTupleType = table_tuple_t; + 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)}; +struct ToTableOrQuery< + SelectFrom> { + dynamic::SelectFrom::TableOrQueryType operator()(const auto& _query) { + using TableTupleType = + table_tuple_t; + return Ref::make( + transpilation::to_select_from( + _query.fields_, _query.from_, _query.joins_, _query.where_, + _query.limit_)); } }; } // namespace transpilation -template +template inline auto select_from(const FieldTypes&... _fields) { using FieldsType = rfl::Tuple::Type...>; - return SelectFrom{ + return SelectFrom, Nothing, + FieldsType>{ .fields_ = - FieldsType(internal::GetColType::get_value(_fields)...)}; + FieldsType(internal::GetColType::get_value(_fields)...), + .from_ = transpilation::TableWrapper{}}; } -template inline auto select_from(const FieldTypes&... _fields) { using FieldsType = rfl::Tuple::Type...>; - return SelectFrom, FieldsType>{ + return SelectFrom, Literal<_alias>, + FieldsType>{ .fields_ = - FieldsType(internal::GetColType::get_value(_fields)...)}; + FieldsType(internal::GetColType::get_value(_fields)...), + .from_ = transpilation::TableWrapper{}}; +} + +template +inline auto select_from(const QueryType& _query, const FieldTypes&... _fields) { + using FieldsType = + rfl::Tuple::Type...>; + return SelectFrom, FieldsType>{ + .fields_ = + FieldsType(internal::GetColType::get_value(_fields)...), + .from_ = _query}; } } // namespace sqlgen diff --git a/include/sqlgen/transpilation/get_table_t.hpp b/include/sqlgen/transpilation/get_table_t.hpp index 306034f..34e98e9 100644 --- a/include/sqlgen/transpilation/get_table_t.hpp +++ b/include/sqlgen/transpilation/get_table_t.hpp @@ -8,6 +8,7 @@ #include "../Literal.hpp" #include "../dynamic/JoinType.hpp" +#include "TableWrapper.hpp" namespace sqlgen::transpilation { @@ -45,6 +46,11 @@ struct GetTableType, "Alias could not be identified."); }; +template +struct GetTableType, TableWrapper> { + using TableType = T; +}; + template struct GetTableType, T> { using TableType = T; @@ -60,6 +66,11 @@ struct GetTableType, "Alias could not be identified."); }; +template +struct GetTableType, TableWrapper> { + using TableType = T; +}; + template struct GetTableType, T> { using TableType = T; diff --git a/include/sqlgen/transpilation/read_to_select_from.hpp b/include/sqlgen/transpilation/read_to_select_from.hpp index fb76e10..a80c703 100644 --- a/include/sqlgen/transpilation/read_to_select_from.hpp +++ b/include/sqlgen/transpilation/read_to_select_from.hpp @@ -42,7 +42,7 @@ dynamic::SelectFrom read_to_select_from(const WhereType& _where = WhereType{}, })); return dynamic::SelectFrom{ - .table = + .table_or_query = dynamic::Table{.name = get_tablename(), .schema = get_schema()}, .fields = fields, .where = to_condition>(_where), diff --git a/include/sqlgen/transpilation/table_tuple_t.hpp b/include/sqlgen/transpilation/table_tuple_t.hpp index d5992c0..787b2ae 100644 --- a/include/sqlgen/transpilation/table_tuple_t.hpp +++ b/include/sqlgen/transpilation/table_tuple_t.hpp @@ -11,18 +11,18 @@ namespace sqlgen::transpilation { -template +template struct TableTupleType; -template -struct TableTupleType { - using Type = StructType; +template +struct TableTupleType { + using Type = TableOrQueryType; }; -template -struct TableTupleType> { +template +struct TableTupleType> { using Type = rfl::Tuple< - std::pair, AliasType>, + std::pair, AliasType>, std::pair, typename JoinTypes::Alias>...>; static_assert( @@ -30,9 +30,9 @@ struct TableTupleType> { "Your SELECT FROM query cannot contain duplicate aliases."); }; -template +template using table_tuple_t = - typename TableTupleType, + typename TableTupleType, std::remove_cvref_t, std::remove_cvref_t>::Type; diff --git a/include/sqlgen/transpilation/to_joins.hpp b/include/sqlgen/transpilation/to_joins.hpp index 0085338..9a20d4d 100644 --- a/include/sqlgen/transpilation/to_joins.hpp +++ b/include/sqlgen/transpilation/to_joins.hpp @@ -19,6 +19,19 @@ namespace sqlgen::transpilation { template struct ToJoin; +template +struct ToJoin { + template + dynamic::Join operator()( + const transpilation::Join& _join) { + return dynamic::Join{ + .how = _join.how, + .table_or_query = to_table_or_query(_join.table_or_query), + .alias = Literal<_alias>().str(), + .on = to_condition(_join.on)}; + } +}; + template struct ToJoin> { template diff --git a/include/sqlgen/transpilation/to_select_from.hpp b/include/sqlgen/transpilation/to_select_from.hpp index 62df16d..08877d1 100644 --- a/include/sqlgen/transpilation/to_select_from.hpp +++ b/include/sqlgen/transpilation/to_select_from.hpp @@ -15,11 +15,10 @@ #include "../dynamic/Table.hpp" #include "../internal/collect/vector.hpp" #include "Join.hpp" +#include "TableWrapper.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" @@ -27,13 +26,15 @@ #include "to_joins.hpp" #include "to_limit.hpp" #include "to_order_by.hpp" +#include "to_table_or_query.hpp" namespace sqlgen::transpilation { template + class TableOrQueryType, class JoinsType, class WhereType, + class GroupByType, class OrderByType, class LimitType> dynamic::SelectFrom to_select_from(const FieldsType& _fields, + const TableOrQueryType& _table_or_query, const JoinsType& _joins, const WhereType& _where, const LimitType& _limit) { @@ -43,17 +44,12 @@ dynamic::SelectFrom to_select_from(const FieldsType& _fields, "The aggregations were not set up correctly. Please check the " "trace for a more detailed error message."); - using StructType = - get_table_t, TableTupleType>; - const auto fields = make_fields( _fields, std::make_integer_sequence>()); return dynamic::SelectFrom{ - .table = dynamic::Table{.alias = to_alias(), - .name = get_tablename(), - .schema = get_schema()}, + .table_or_query = to_table_or_query(_table_or_query), .fields = fields, .alias = to_alias(), .joins = to_joins(_joins), diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index ce5a8fd..a75efd0 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -77,17 +77,20 @@ struct ToSQL> { } }; -template -struct ToSQL> { +template +struct ToSQL< + SelectFrom> { dynamic::Statement operator()(const auto& _select_from) const { - using TableTupleType = table_tuple_t; - return to_select_from( - _select_from.fields_, _select_from.joins_, _select_from.where_, - _select_from.limit_); + using TableTupleType = + table_tuple_t; + return to_select_from( + _select_from.fields_, _select_from.from_, _select_from.joins_, + _select_from.where_, _select_from.limit_); } }; diff --git a/include/sqlgen/transpilation/to_table_or_query.hpp b/include/sqlgen/transpilation/to_table_or_query.hpp new file mode 100644 index 0000000..3b89dbc --- /dev/null +++ b/include/sqlgen/transpilation/to_table_or_query.hpp @@ -0,0 +1,34 @@ +#ifndef SQLGEN_TRANSPILATION_TO_TABLE_OR_QUERY_HPP_ +#define SQLGEN_TRANSPILATION_TO_TABLE_OR_QUERY_HPP_ + +#include +#include + +#include "../dynamic/SelectFrom.hpp" +#include "../dynamic/Table.hpp" +#include "TableWrapper.hpp" +#include "get_schema.hpp" +#include "get_tablename.hpp" + +namespace sqlgen::transpilation { + +template +struct ToTableOrQuery; + +template +struct ToTableOrQuery> { + dynamic::SelectFrom::TableOrQueryType operator()(const auto&) { + using T = std::remove_cvref_t; + return dynamic::Table{.name = get_tablename(), + .schema = get_schema()}; + } +}; + +template +dynamic::SelectFrom::TableOrQueryType to_table_or_query(const T& _t) { + return ToTableOrQuery{}(_t); +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index e67730e..19e2709 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -65,6 +65,9 @@ std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept; std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept; + std::string type_to_sql(const dynamic::Type& _type) noexcept; std::string update_to_sql(const dynamic::Update& _stmt) noexcept; @@ -508,16 +511,7 @@ std::string join_to_sql(const dynamic::Join& _stmt) noexcept { 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 << table_or_query_to_sql(_stmt.table_or_query) << " "; stream << _stmt.alias << " "; @@ -713,11 +707,7 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << internal::strings::join( ", ", internal::collect::vector(_stmt.fields | transform(field_to_str))); - stream << " FROM "; - if (_stmt.table.schema) { - stream << wrap_in_quotes(*_stmt.table.schema) << "."; - } - stream << wrap_in_quotes(_stmt.table.name); + stream << " FROM " << table_or_query_to_sql(_stmt.table_or_query); if (_stmt.alias) { stream << " " << *_stmt.alias; @@ -756,6 +746,18 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { return stream.str(); } +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept { + return _table_or_query.visit([](const auto& _t) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return wrap_in_quotes(_t.name); + } else { + return "(" + select_from_to_sql(*_t) + ")"; + } + }); +} + std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { return _stmt.visit([&](const auto& _s) -> std::string { using S = std::remove_cvref_t; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 861cea0..2223ef6 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -51,6 +51,9 @@ std::string properties_to_sql( std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept; + std::string type_to_sql(const dynamic::Type& _type) noexcept; std::string update_to_sql(const dynamic::Update& _stmt) noexcept; @@ -383,20 +386,8 @@ std::string join_to_sql(const dynamic::Join& _stmt) noexcept { 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 << " "; + << " " << table_or_query_to_sql(_stmt.table_or_query) << " " + << _stmt.alias << " "; if (_stmt.on) { stream << "ON " << condition_to_sql(*_stmt.on); @@ -599,11 +590,7 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << internal::strings::join( ", ", internal::collect::vector(_stmt.fields | transform(field_to_str))); - stream << " FROM "; - if (_stmt.table.schema) { - stream << wrap_in_quotes(*_stmt.table.schema) << "."; - } - stream << wrap_in_quotes(_stmt.table.name); + stream << " FROM " << table_or_query_to_sql(_stmt.table_or_query); if (_stmt.alias) { stream << " " << *_stmt.alias; @@ -642,6 +629,18 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { return stream.str(); } +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept { + return _table_or_query.visit([](const auto& _t) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return wrap_in_quotes(_t.name); + } else { + return "(" + select_from_to_sql(*_t) + ")"; + } + }); +} + std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { return _stmt.visit([&](const auto& _s) -> std::string { using S = std::remove_cvref_t; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index d7297c8..f963bb7 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -46,6 +46,9 @@ std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept; std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept; + std::string type_to_sql(const dynamic::Type& _type) noexcept; std::string update_to_sql(const dynamic::Update& _stmt) noexcept; @@ -384,16 +387,7 @@ std::string join_to_sql(const dynamic::Join& _stmt) noexcept { 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 << table_or_query_to_sql(_stmt.table_or_query) << " "; stream << _stmt.alias << " "; @@ -607,11 +601,7 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << internal::strings::join( ", ", internal::collect::vector(_stmt.fields | transform(field_to_str))); - stream << " FROM "; - if (_stmt.table.schema) { - stream << wrap_in_quotes(*_stmt.table.schema) << "."; - } - stream << wrap_in_quotes(_stmt.table.name); + stream << " FROM " << table_or_query_to_sql(_stmt.table_or_query); if (_stmt.alias) { stream << " " << *_stmt.alias; @@ -650,6 +640,18 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { return stream.str(); } +std::string table_or_query_to_sql( + const dynamic::SelectFrom::TableOrQueryType& _table_or_query) noexcept { + return _table_or_query.visit([](const auto& _t) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return wrap_in_quotes(_t.name); + } else { + return "(" + select_from_to_sql(*_t) + ")"; + } + }); +} + std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept { return _stmt.visit([&](const auto& _s) -> std::string { using S = std::remove_cvref_t; diff --git a/tests/mysql/test_joins_from.cpp b/tests/mysql/test_joins_from.cpp new file mode 100644 index 0000000..5888620 --- /dev/null +++ b/tests/mysql/test_joins_from.cpp @@ -0,0 +1,94 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_from { + +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(mysql, test_joins_from) { + 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::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_parents = + select_from( + "child_id"_t2 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, "age"_t1 | as<"age">) | + left_join("id"_t1 == "parent_id"_t2); + + const auto get_people = + select_from<"t1">(get_parents, "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("id"_t1 == "id"_t2) | + order_by("id"_t2, "id"_t1) | to>; + + const auto people = mysql::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 (SELECT t2.`child_id` AS `id`, t1.`first_name` AS `first_name`, t1.`last_name` AS `last_name`, t1.`age` AS `age` FROM `Person` t1 LEFT JOIN `Relationship` t2 ON t1.`id` = t2.`parent_id`) t1 INNER JOIN `Person` t2 ON t1.`id` = t2.`id` ORDER BY t2.`id`, t1.`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":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.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":"Maggie","parent_age_at_birth":40.0}])"; + + EXPECT_EQ(mysql::to_sql(get_people), expected_query); + EXPECT_EQ(rfl::json::write(people), expected); +} + +} // namespace test_joins_from + +#endif diff --git a/tests/postgres/test_joins_from.cpp b/tests/postgres/test_joins_from.cpp new file mode 100644 index 0000000..8613eb2 --- /dev/null +++ b/tests/postgres/test_joins_from.cpp @@ -0,0 +1,94 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_from { + +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_from) { + 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; + using namespace sqlgen::literals; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_parents = + select_from( + "child_id"_t2 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, "age"_t1 | as<"age">) | + left_join("id"_t1 == "parent_id"_t2); + + const auto get_people = + select_from<"t1">(get_parents, "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("id"_t1 == "id"_t2) | + order_by("id"_t2, "id"_t1) | 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 (SELECT t2."child_id" AS "id", t1."first_name" AS "first_name", t1."last_name" AS "last_name", t1."age" AS "age" FROM "Person" t1 LEFT JOIN "Relationship" t2 ON t1."id" = t2."parent_id") t1 INNER JOIN "Person" t2 ON t1."id" = t2."id" ORDER BY t2."id", t1."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":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.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":"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_from + +#endif diff --git a/tests/sqlite/test_joins_from.cpp b/tests/sqlite/test_joins_from.cpp new file mode 100644 index 0000000..aa4fedc --- /dev/null +++ b/tests/sqlite/test_joins_from.cpp @@ -0,0 +1,86 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_joins_from { + +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_from) { + 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; + using namespace sqlgen::literals; + + struct ParentAndChild { + std::string last_name; + std::string first_name_parent; + std::string first_name_child; + double parent_age_at_birth; + }; + + const auto get_parents = + select_from( + "child_id"_t2 | as<"id">, "first_name"_t1 | as<"first_name">, + "last_name"_t1 | as<"last_name">, "age"_t1 | as<"age">) | + left_join("id"_t1 == "parent_id"_t2); + + const auto get_people = + select_from<"t1">(get_parents, "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("id"_t1 == "id"_t2) | + order_by("id"_t2, "id"_t1) | to>; + + const auto people = sqlite::connect() + .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 (SELECT t2."child_id" AS "id", t1."first_name" AS "first_name", t1."last_name" AS "last_name", t1."age" AS "age" FROM "Person" t1 LEFT JOIN "Relationship" t2 ON t1."id" = t2."parent_id") t1 INNER JOIN "Person" t2 ON t1."id" = t2."id" ORDER BY t2."id", t1."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":"Marge","first_name_child":"Bart","parent_age_at_birth":30.0},{"last_name":"Simpson","first_name_parent":"Homer","first_name_child":"Lisa","parent_age_at_birth":37.0},{"last_name":"Simpson","first_name_parent":"Marge","first_name_child":"Lisa","parent_age_at_birth":32.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":"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_from + diff --git a/tests/sqlite/test_unique.cpp b/tests/sqlite/test_unique.cpp index bf52f03..ad65a9e 100644 --- a/tests/sqlite/test_unique.cpp +++ b/tests/sqlite/test_unique.cpp @@ -16,7 +16,7 @@ struct Person { double age; }; -TEST(postgres, test_unique) { +TEST(sqlite, test_unique) { const auto people = std::vector( {Person{ .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},