diff --git a/docs/duckdb.md b/docs/duckdb.md index 09ba808..abc50d5 100644 --- a/docs/duckdb.md +++ b/docs/duckdb.md @@ -131,7 +131,7 @@ query(conn).value(); - Resource management through `Ref` - Auto-incrementing primary keys - Various data types including VARCHAR, TIMESTAMP, DATE - - Complex queries with WHERE clauses, ORDER BY, LIMIT, JOINs + - Complex queries with WHERE clauses, ORDER BY, LIMIT, OFFSET, JOINs - LIKE and pattern matching operations - Mathematical operations and string functions - JSON data types diff --git a/docs/group_by_and_aggregations.md b/docs/group_by_and_aggregations.md index 8b13efd..25f043b 100644 --- a/docs/group_by_and_aggregations.md +++ b/docs/group_by_and_aggregations.md @@ -216,10 +216,9 @@ In this example, each field in the `Result` struct must have a name that matches ## Notes -- The `group_by` clause must be used before `order_by` or `limit` clauses +- The `group_by` clause must be used before `order_by` or `limit` and `offset` clauses - You cannot use `group_by` multiple times in the same query - Aggregation functions can be used with or without `group_by` - The result type must match the structure of your select statement. Note that fields are matched by name, not order. - All aggregations are type-safe and will map to appropriate SQL types - The `Result<...>` type provides error handling; use `.value()` to extract the result (will throw an exception if there's an error) or handle errors as needed - diff --git a/docs/literals.md b/docs/literals.md index db93864..36de143 100644 --- a/docs/literals.md +++ b/docs/literals.md @@ -113,7 +113,8 @@ using namespace sqlgen::literals; const auto query = sqlgen::read> | where("age"_c >= 18 and "last_name"_c == "Simpson") | order_by("first_name"_c.desc()) - | limit(10); + | limit(10) + | offset(3); ``` This generates: @@ -123,5 +124,6 @@ SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE ("age" >= 18) AND ("last_name" = 'Simpson') ORDER BY "first_name" DESC -LIMIT 10; +LIMIT 10 +OFFSET 3; ``` diff --git a/docs/mysql.md b/docs/mysql.md index 87edb17..cc97565 100644 --- a/docs/mysql.md +++ b/docs/mysql.md @@ -170,7 +170,6 @@ const auto result = session(pool) - Connection pooling for high-performance applications - Auto-incrementing primary keys - Various data types including VARCHAR, TIMESTAMP, DATE - - Complex queries with WHERE clauses, ORDER BY, LIMIT, JOINs + - Complex queries with WHERE clauses, ORDER BY, LIMIT, OFFSET, JOINs - LIKE and pattern matching operations - Mathematical operations and string functions - diff --git a/docs/reading.md b/docs/reading.md index 73e74d3..44dc703 100644 --- a/docs/reading.md +++ b/docs/reading.md @@ -97,6 +97,32 @@ ORDER BY "age" LIMIT 2; ``` +You can also combine `limit` with `offset` to perform paging: + +```cpp +using namespace sqlgen; +using namespace sqlgen::literals; + +const auto query = sqlgen::read> | + order_by("age"_c) | + limit(2) | + offset(3); + +const auto skip_three = query(conn).value(); +``` + +This generates the following SQL: + +```sql +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +ORDER BY "age" +LIMIT 2 +OFFSET 3; +``` + +- **SQLite and MySql Limitation*: You cannot use `offset` without `limit`. + ### With ranges Read results as a lazy range: diff --git a/docs/select_from.md b/docs/select_from.md index 35dd034..ac9e055 100644 --- a/docs/select_from.md +++ b/docs/select_from.md @@ -136,10 +136,11 @@ 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 +| offset(5) // Offset the result set | to>; // Convert to container ``` diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index d79a4f5..13c8c0d 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -36,6 +36,7 @@ #include "sqlgen/is_connection.hpp" #include "sqlgen/joins.hpp" #include "sqlgen/limit.hpp" +#include "sqlgen/offset.hpp" #include "sqlgen/literals.hpp" #include "sqlgen/operations.hpp" #include "sqlgen/order_by.hpp" diff --git a/include/sqlgen/create_as.hpp b/include/sqlgen/create_as.hpp index 1dd2bb2..2f217b4 100644 --- a/include/sqlgen/create_as.hpp +++ b/include/sqlgen/create_as.hpp @@ -18,12 +18,12 @@ namespace sqlgen { template + class OrderByType, class LimitType, class OffsetType, class ToType, class Connection> requires is_connection Result> create_as_impl( const Ref& _conn, const dynamic::CreateAs::What _what, const SelectFrom& + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>& _as, const bool _or_replace, const bool _if_not_exists) { using TableTupleType = @@ -31,9 +31,9 @@ Result> create_as_impl( const auto query = transpilation::to_create_as< ValueType, TableTupleType, AliasType, FieldsType, TableOrQueryType, - JoinsType, WhereType, GroupByType, OrderByType, LimitType>( + JoinsType, WhereType, GroupByType, OrderByType, LimitType, OffsetType>( _what, _or_replace, _if_not_exists, _as.fields_, _as.from_, _as.joins_, - _as.where_, _as.limit_); + _as.where_, _as.limit_, _as.offset_); return _conn->execute(_conn->to_sql(query)).transform([&](const auto&) { return _conn; @@ -42,12 +42,12 @@ Result> create_as_impl( template + class OrderByType, class LimitType, class OffsetType, class ToType, class Connection> requires is_connection Result> create_as_impl( const Result>& _res, const dynamic::CreateAs::What _what, const SelectFrom& + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>& _as, const bool _or_replace, const bool _if_not_exists) { return _res.and_then([&](const auto& _conn) { @@ -58,10 +58,10 @@ Result> create_as_impl( template + class OrderByType, class LimitType, class OffsetType, class ToType> struct CreateAs { using As = SelectFrom; + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>; static_assert( requires(transpilation::extract_table_t a) { @@ -108,4 +108,3 @@ inline auto create_or_replace_view_as(const SelectFrom& _as) { }; // namespace sqlgen #endif - diff --git a/include/sqlgen/duckdb/Connection.hpp b/include/sqlgen/duckdb/Connection.hpp index 19d9b7a..12db37f 100644 --- a/include/sqlgen/duckdb/Connection.hpp +++ b/include/sqlgen/duckdb/Connection.hpp @@ -139,7 +139,7 @@ class SQLGEN_API Connection { })); const auto select_from = dynamic::SelectFrom{ - .table_or_query = _table, .fields = fields, .limit = dynamic::Limit{0}}; + .table_or_query = _table, .fields = fields, .limit = dynamic::Limit{0}, .offset = dynamic::Offset{0}}; return DuckDBResult::make(to_sql(select_from), conn_) .transform([&](const auto &_res) { diff --git a/include/sqlgen/dynamic/Offset.hpp b/include/sqlgen/dynamic/Offset.hpp new file mode 100644 index 0000000..bae6d60 --- /dev/null +++ b/include/sqlgen/dynamic/Offset.hpp @@ -0,0 +1,14 @@ +#ifndef SQLGEN_DYNAMIC_OFFSET_HPP_ +#define SQLGEN_DYNAMIC_OFFSET_HPP_ + +#include + +namespace sqlgen::dynamic { + +struct Offset { + size_t val; +}; + +} // namespace sqlgen::dynamic + +#endif diff --git a/include/sqlgen/dynamic/SelectFrom.hpp b/include/sqlgen/dynamic/SelectFrom.hpp index 31d3e74..da28f1c 100644 --- a/include/sqlgen/dynamic/SelectFrom.hpp +++ b/include/sqlgen/dynamic/SelectFrom.hpp @@ -11,6 +11,7 @@ #include "GroupBy.hpp" #include "JoinType.hpp" #include "Limit.hpp" +#include "Offset.hpp" #include "Operation.hpp" #include "OrderBy.hpp" #include "Table.hpp" @@ -46,6 +47,7 @@ struct SelectFrom { std::optional group_by = std::nullopt; std::optional order_by = std::nullopt; std::optional limit = std::nullopt; + std::optional offset = std::nullopt; }; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/offset.hpp b/include/sqlgen/offset.hpp new file mode 100644 index 0000000..5c6b9f7 --- /dev/null +++ b/include/sqlgen/offset.hpp @@ -0,0 +1,14 @@ +#ifndef SQLGEN_OFFSET_HPP_ +#define SQLGEN_OFFSET_HPP_ + +#include "transpilation/Offset.hpp" + +namespace sqlgen { + +using Offset = transpilation::Offset; + +inline auto offset(const size_t _val) { return Offset{_val}; }; + +} // namespace sqlgen + +#endif diff --git a/include/sqlgen/read.hpp b/include/sqlgen/read.hpp index 983ab57..a2111ae 100644 --- a/include/sqlgen/read.hpp +++ b/include/sqlgen/read.hpp @@ -9,6 +9,7 @@ #include "internal/is_range.hpp" #include "is_connection.hpp" #include "limit.hpp" +#include "offset.hpp" #include "order_by.hpp" #include "transpilation/order_by_t.hpp" #include "transpilation/read_to_select_from.hpp" @@ -18,40 +19,40 @@ namespace sqlgen { template + class LimitType, class OffsetType, class Connection> requires is_connection auto read_impl(const Ref& _conn, const WhereType& _where, - const LimitType& _limit) { + const LimitType& _limit, const OffsetType& _offset) { using ValueType = transpilation::value_t; const auto query = transpilation::read_to_select_from(_where, _limit); + LimitType, OffsetType>(_where, _limit, _offset); return _conn->template read(query); } template + class LimitType, class OffsetType, class Connection> requires is_connection auto read_impl(const Result>& _res, const WhereType& _where, - const LimitType& _limit) { + const LimitType& _limit, const OffsetType& _offset) { return _res.and_then([&](const auto& _conn) { - return read_impl( - _conn, _where, _limit); + return read_impl( + _conn, _where, _limit, _offset); }); } template + class LimitType = Nothing, class OffsetType = Nothing> struct Read { auto operator()(const auto& _conn) const { if constexpr (std::ranges::input_range> || internal::is_range_v) { - return read_impl(_conn, where_, - limit_); + return read_impl(_conn, where_, + limit_, offset_); } else { - return read_impl, WhereType, OrderByType, LimitType>( - _conn, where_, limit_) + return read_impl, WhereType, OrderByType, LimitType, OffsetType>( + _conn, where_, limit_, offset_) .and_then([](auto&& _vec) -> Result { if (_vec.size() != 1) { return error( @@ -73,7 +74,9 @@ struct Read { "You cannot call order_by(...) before where(...)."); static_assert(std::is_same_v, "You cannot call limit(...) before where(...)."); - return Read{ + static_assert(std::is_same_v, + "You cannot call offset(...) before where(...)."); + return Read{ .where_ = _where.condition}; } @@ -85,25 +88,38 @@ struct Read { "than one column)."); static_assert(std::is_same_v, "You cannot call limit(...) before order_by."); + static_assert(std::is_same_v, + "You cannot call offset(...) before order_by."); static_assert(sizeof...(ColTypes) != 0, "You must assign at least one column to order by(...)."); return Read, Nothing, typename std::remove_cvref_t::ColType...>, - LimitType>{.where_ = _r.where_}; + LimitType, OffsetType>{.where_ = _r.where_}; } friend auto operator|(const Read& _r, const Limit& _limit) { static_assert(std::is_same_v, "You cannot call limit(...) twice."); - return Read{.where_ = _r.where_, - .limit_ = _limit}; + return Read{.where_ = _r.where_, + .limit_ = _limit, + .offset_ = _r.offset_}; + } + + friend auto operator|(const Read& _r, const Offset& _offset) { + static_assert(std::is_same_v, + "You cannot call offset(...) twice."); + return Read{.where_ = _r.where_, + .limit_ = _r.limit_, + .offset_ = _offset}; } WhereType where_; LimitType limit_; + + OffsetType offset_; }; template diff --git a/include/sqlgen/select_from.hpp b/include/sqlgen/select_from.hpp index dddc4c3..5173e76 100644 --- a/include/sqlgen/select_from.hpp +++ b/include/sqlgen/select_from.hpp @@ -17,6 +17,7 @@ #include "internal/iterator_t.hpp" #include "is_connection.hpp" #include "limit.hpp" +#include "offset.hpp" #include "order_by.hpp" #include "to.hpp" #include "transpilation/Join.hpp" @@ -36,10 +37,10 @@ template requires is_connection auto select_from_impl(const Ref& _conn, const auto& _fields, const auto& _table_or_query, const auto& _joins, - const auto& _where, const auto& _limit) { + const auto& _where, const auto& _limit, const auto& _offset) { if constexpr (internal::is_range_v) { const auto query = transpilation::to_select_from( - _fields, _table_or_query, _joins, _where, _limit); + _fields, _table_or_query, _joins, _where, _limit, _offset); return _conn->template read(query); } else { @@ -66,7 +67,7 @@ auto select_from_impl(const Ref& _conn, const auto& _fields, using RangeType = Range; return select_from_impl( - _conn, _fields, _table_or_query, _joins, _where, _limit) + _conn, _fields, _table_or_query, _joins, _where, _limit, _offset) .and_then(to_container); } } @@ -75,17 +76,17 @@ template requires is_connection auto select_from_impl(const Result>& _res, const auto& _fields, const auto& _table_or_query, const auto& _joins, - const auto& _where, const auto& _limit) { + const auto& _where, const auto& _limit, const auto& _offset) { return _res.and_then([&](const auto& _conn) { return select_from_impl( - _conn, _fields, _table_or_query, _joins, _where, _limit); + _conn, _fields, _table_or_query, _joins, _where, _limit, _offset); }); } template + class LimitT = Nothing, class OffsetT = Nothing, class ToT = Nothing> struct SelectFrom { using TableOrQueryType = TableOrQueryT; using AliasType = AliasT; @@ -95,12 +96,13 @@ struct SelectFrom { using GroupByType = GroupByT; using OrderByType = OrderByT; using LimitType = LimitT; + using OffsetType = OffsetT; using ToType = ToT; using SelectFromTypes = transpilation::SelectFromTypes; + OrderByType, LimitType, OffsetType>; auto operator()(const auto& _conn) const { using TableTupleType = @@ -115,7 +117,7 @@ struct SelectFrom { using ContainerType = std::conditional_t, Range, ToType>; return select_from_impl( - _conn, fields_, from_, joins_, where_, limit_); + _conn, fields_, from_, joins_, where_, limit_, offset_); } else { const auto extract_result = [](auto&& _vec) -> Result { @@ -130,7 +132,7 @@ struct SelectFrom { return select_from_impl>>( - _conn, fields_, from_, joins_, where_, limit_) + _conn, fields_, from_, joins_, where_, limit_, offset_) .and_then(extract_result); } } @@ -149,6 +151,8 @@ struct SelectFrom { "You cannot call order_by(...) before a join."); static_assert(std::is_same_v, "You cannot call limit(...) before a join."); + static_assert(std::is_same_v, + "You cannot call offset(...) before a join."); static_assert(std::is_same_v, "You cannot call to<...> before a join."); @@ -158,7 +162,7 @@ struct SelectFrom { _alias, _how>>; return SelectFrom{ + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>{ .fields_ = _s.fields_, .from_ = _s.from_, .joins_ = NewJoinsType(_join)}; @@ -173,7 +177,7 @@ struct SelectFrom { using NewJoinsType = std::remove_cvref_t; return SelectFrom{ + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>{ .fields_ = _s.fields_, .from_ = _s.from_, .joins_ = joins}; } } @@ -190,10 +194,12 @@ struct SelectFrom { "You cannot call order_by(...) before where(...)."); static_assert(std::is_same_v, "You cannot call limit(...) before where(...)."); + static_assert(std::is_same_v, + "You cannot call offset(...) before where(...)."); static_assert(std::is_same_v, "You cannot call to<...> before where(...)."); return SelectFrom{.fields_ = _s.fields_, .from_ = _s.from_, .joins_ = _s.joins_, @@ -210,6 +216,8 @@ struct SelectFrom { "You cannot call order_by(...) before group_by(...)."); static_assert(std::is_same_v, "You cannot call limit(...) before group_by(...)."); + static_assert(std::is_same_v, + "You cannot call offset(...) before group_by(...)."); static_assert(std::is_same_v, "You cannot call to<...> before group_by(...)."); static_assert(sizeof...(ColTypes) != 0, @@ -220,7 +228,7 @@ struct SelectFrom { WhereType, transpilation::group_by_t, - OrderByType, LimitType, ToType>{.fields_ = _s.fields_, + OrderByType, LimitType, OffsetType, ToType>{.fields_ = _s.fields_, .from_ = _s.from_, .joins_ = _s.joins_, .where_ = _s.where_}; @@ -234,6 +242,8 @@ struct SelectFrom { "than one column)."); static_assert(std::is_same_v, "You cannot call limit(...) before order_by(...)."); + static_assert(std::is_same_v, + "You cannot call offset(...) before order_by(...)."); static_assert(std::is_same_v, "You cannot call to<...> before order_by(...)."); static_assert(sizeof...(ColTypes) != 0, @@ -247,7 +257,7 @@ struct SelectFrom { typename std::remove_cvref_t::ColType...>; return SelectFrom{.fields_ = _s.fields_, .from_ = _s.from_, .joins_ = _s.joins_, @@ -257,13 +267,27 @@ struct SelectFrom { 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_, + .from_ = _s.from_, + .joins_ = _s.joins_, + .where_ = _s.where_, + .limit_ = _limit, + .offset_ = _s.offset_}; + } + + friend auto operator|(const SelectFrom& _s, const Offset& _offset) { + static_assert(std::is_same_v, + "You cannot call offset twice."); return SelectFrom{ + WhereType, GroupByType, OrderByType, LimitType, Offset, ToType>{ .fields_ = _s.fields_, .from_ = _s.from_, .joins_ = _s.joins_, .where_ = _s.where_, - .limit_ = _limit}; + .limit_ = _s.limit_, + .offset_ = _offset}; } template @@ -271,12 +295,13 @@ struct SelectFrom { static_assert(std::is_same_v, "You cannot call to<...> twice."); return SelectFrom{.fields_ = _s.fields_, .from_ = _s.from_, .joins_ = _s.joins_, .where_ = _s.where_, - .limit_ = _s.limit_}; + .limit_ = _s.limit_, + .offset_ = _s.offset_}; } FieldsType fields_; @@ -288,6 +313,8 @@ struct SelectFrom { WhereType where_; LimitType limit_; + + OffsetType offset_; }; namespace transpilation { @@ -303,16 +330,16 @@ struct ExtractTable< template + class OrderByType, class LimitType, class OffsetType, class ToType> struct ToTableOrQuery< SelectFrom> { + GroupByType, OrderByType, LimitType, OffsetType, ToType>> { dynamic::SelectFrom::TableOrQueryType operator()(const auto& _query) { using QueryType = std::remove_cvref_t; return Ref::make( transpilation::to_select_from( _query.fields_, _query.from_, _query.joins_, _query.where_, - _query.limit_)); + _query.limit_, _query.offset_)); } }; diff --git a/include/sqlgen/transpilation/Offset.hpp b/include/sqlgen/transpilation/Offset.hpp new file mode 100644 index 0000000..e8658f8 --- /dev/null +++ b/include/sqlgen/transpilation/Offset.hpp @@ -0,0 +1,12 @@ +#ifndef SQLGEN_TRANSPILATION_OFFSET_HPP_ +#define SQLGEN_TRANSPILATION_OFFSET_HPP_ + +#include "../dynamic/Offset.hpp" + +namespace sqlgen::transpilation { + +using Offset = dynamic::Offset; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/read_to_select_from.hpp b/include/sqlgen/transpilation/read_to_select_from.hpp index 834acfd..47cdf92 100644 --- a/include/sqlgen/transpilation/read_to_select_from.hpp +++ b/include/sqlgen/transpilation/read_to_select_from.hpp @@ -18,16 +18,18 @@ #include "make_columns.hpp" #include "to_condition.hpp" #include "to_limit.hpp" +#include "to_offset.hpp" #include "to_order_by.hpp" namespace sqlgen::transpilation { template + class LimitType = Nothing, class OffsetType = Nothing> requires std::is_class_v> && std::is_aggregate_v> dynamic::SelectFrom read_to_select_from(const WhereType& _where = WhereType{}, - const LimitType& _limit = LimitType{}) { + const LimitType& _limit = LimitType{}, + const OffsetType& _offset = OffsetType{}) { using namespace std::ranges::views; using NamedTupleType = rfl::named_tuple_t>; @@ -48,7 +50,8 @@ dynamic::SelectFrom read_to_select_from(const WhereType& _where = WhereType{}, .fields = fields, .where = to_condition>(_where), .order_by = to_order_by(), - .limit = to_limit(_limit)}; + .limit = to_limit(_limit), + .offset = to_offset(_offset)}; } } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/to_create_as.hpp b/include/sqlgen/transpilation/to_create_as.hpp index 55c3ed4..83c9a67 100644 --- a/include/sqlgen/transpilation/to_create_as.hpp +++ b/include/sqlgen/transpilation/to_create_as.hpp @@ -17,7 +17,7 @@ namespace sqlgen::transpilation { template + class GroupByType, class OrderByType, class LimitType, class OffsetType> requires std::is_class_v> && std::is_aggregate_v> dynamic::CreateAs to_create_as(const dynamic::CreateAs::What _what, @@ -26,11 +26,11 @@ dynamic::CreateAs to_create_as(const dynamic::CreateAs::What _what, const FieldsType& _fields, const TableOrQueryType& _table_or_query, const JoinsType& _joins, const WhereType& _where, - const LimitType& _limit) { + const LimitType& _limit, const OffsetType& _offset) { using SelectFromTypes = transpilation::SelectFromTypes; + OrderByType, LimitType, OffsetType>; return dynamic::CreateAs{ .what = _what, @@ -38,7 +38,7 @@ dynamic::CreateAs to_create_as(const dynamic::CreateAs::What _what, .name = get_tablename(), .schema = get_schema()}, .query = to_select_from(_fields, _table_or_query, _joins, - _where, _limit), + _where, _limit, _offset), .or_replace = _or_replace, .if_not_exists = _if_not_exists}; } diff --git a/include/sqlgen/transpilation/to_offset.hpp b/include/sqlgen/transpilation/to_offset.hpp new file mode 100644 index 0000000..3145255 --- /dev/null +++ b/include/sqlgen/transpilation/to_offset.hpp @@ -0,0 +1,30 @@ +#ifndef SQLGEN_TRANSPILATION_TO_OFFSET_HPP_ +#define SQLGEN_TRANSPILATION_TO_OFFSET_HPP_ + +#include +#include +#include + +#include "../Result.hpp" +#include "../dynamic/Offset.hpp" +#include "Offset.hpp" +#include "order_by_t.hpp" + +namespace sqlgen::transpilation { + +template +std::optional to_offset(const OffsetType& _offset) { + if constexpr (std::is_same_v, Nothing>) { + return std::nullopt; + + } else if constexpr (std::is_same_v, Offset>) { + return _offset; + + } else { + static_assert(rfl::always_false_v, "Unsupported type"); + } +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/to_select_from.hpp b/include/sqlgen/transpilation/to_select_from.hpp index d453d5c..8072cc5 100644 --- a/include/sqlgen/transpilation/to_select_from.hpp +++ b/include/sqlgen/transpilation/to_select_from.hpp @@ -26,13 +26,14 @@ #include "to_group_by.hpp" #include "to_joins.hpp" #include "to_limit.hpp" +#include "to_offset.hpp" #include "to_order_by.hpp" #include "to_table_or_query.hpp" namespace sqlgen::transpilation { template + class WhereT, class GroupByT, class OrderByT, class LimitT, class OffsetT> struct SelectFromTypes { using AliasType = AliasT; using FieldsType = FieldsT; @@ -42,6 +43,7 @@ struct SelectFromTypes { using GroupByType = GroupByT; using OrderByType = OrderByT; using LimitType = LimitT; + using OffsetType = OffsetT; using TableTupleType = table_tuple_t; }; @@ -50,7 +52,7 @@ template dynamic::SelectFrom to_select_from(const auto& _fields, const auto& _table_or_query, const auto& _joins, const auto& _where, - const auto& _limit) { + const auto& _limit, const auto& _offset) { using TableTupleType = typename SelectFromT::TableTupleType; using AliasType = typename SelectFromT::AliasType; using FieldsType = typename SelectFromT::FieldsType; @@ -75,7 +77,8 @@ dynamic::SelectFrom to_select_from(const auto& _fields, .where = to_condition>(_where), .group_by = to_group_by(), .order_by = to_order_by(), - .limit = to_limit(_limit)}; + .limit = to_limit(_limit), + .offset = to_offset(_offset)}; } } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index 9b11cc4..e479dc1 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -34,10 +34,10 @@ struct ToSQL; template + class OrderByType, class LimitType, class OffsetType, class ToType> struct ToSQL< CreateAs> { + WhereType, GroupByType, OrderByType, LimitType, OffsetType, ToType>> { dynamic::Statement operator()(const auto& _create_as) const { using TableTupleType = transpilation::table_tuple_t; @@ -46,7 +46,7 @@ struct ToSQL< OrderByType, LimitType>( _create_as.what_, _create_as.or_replace_, _create_as.if_not_exists_, _create_as.as_.fields_, _create_as.as_.from_, _create_as.as_.joins_, - _create_as.as_.where_, _create_as.as_.limit_); + _create_as.as_.where_, _create_as.as_.limit_, _create_as.as_.offset_); } }; @@ -90,11 +90,11 @@ struct ToSQL> { }; template -struct ToSQL> { + class LimitType, class OffsetType> +struct ToSQL> { dynamic::Statement operator()(const auto& _read) const { return read_to_select_from, WhereType, OrderByType, - LimitType>(_read.where_, _read.limit_); + LimitType, OffsetType>(_read.where_, _read.limit_, _read.offset_); } }; @@ -104,7 +104,7 @@ struct ToSQL> { using SelectFromTypes = typename SelectFrom::SelectFromTypes; return to_select_from( _select_from.fields_, _select_from.from_, _select_from.joins_, - _select_from.where_, _select_from.limit_); + _select_from.where_, _select_from.limit_, _select_from.offset_); } }; diff --git a/include/sqlgen/transpilation/to_union.hpp b/include/sqlgen/transpilation/to_union.hpp index 7adff15..f618d80 100644 --- a/include/sqlgen/transpilation/to_union.hpp +++ b/include/sqlgen/transpilation/to_union.hpp @@ -27,7 +27,7 @@ dynamic::Union to_union(const rfl::Tuple& _stmts, auto vec = std::vector( {to_select_from( _stmt.fields_, _stmt.from_, _stmt.joins_, _stmt.where_, - _stmt.limit_)...}); + _stmt.limit_, _stmt.offset_)...}); return Ref>::make(std::move(vec)); }, _stmts); diff --git a/src/sqlgen/duckdb/to_sql.cpp b/src/sqlgen/duckdb/to_sql.cpp index bbd92ac..3769f54 100644 --- a/src/sqlgen/duckdb/to_sql.cpp +++ b/src/sqlgen/duckdb/to_sql.cpp @@ -788,6 +788,10 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } + if (_stmt.offset) { + stream << " OFFSET " << _stmt.offset->val; + } + return stream.str(); } diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index 9afb4e0..2346f91 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -827,6 +827,10 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } + if (_stmt.offset) { + stream << " OFFSET " << _stmt.offset->val; + } + return stream.str(); } diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 8eeac73..d2a4777 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -752,6 +752,10 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } + if (_stmt.offset) { + stream << " OFFSET " << _stmt.offset->val; + } + return stream.str(); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 11551c7..c93da21 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -710,6 +710,10 @@ std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { stream << " LIMIT " << _stmt.limit->val; } + if (_stmt.offset) { + stream << " OFFSET " << _stmt.offset->val; + } + return stream.str(); } diff --git a/tests/duckdb/test_offset.cpp b/tests/duckdb/test_offset.cpp new file mode 100644 index 0000000..9bc5356 --- /dev/null +++ b/tests/duckdb/test_offset.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_offset { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(duckdb, test_offset) { + 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}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query = + sqlgen::read> | order_by("id"_c) | offset(3); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_offset diff --git a/tests/mysql/test_offset.cpp b/tests/mysql/test_offset.cpp new file mode 100644 index 0000000..7bd857d --- /dev/null +++ b/tests/mysql/test_offset.cpp @@ -0,0 +1,57 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_offset { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(mysql, test_offset) { + 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}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = + sqlgen::mysql::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | order_by("id"_c) | limit(-1) | offset(3); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_offset + +#endif diff --git a/tests/postgres/test_offset.cpp b/tests/postgres/test_offset.cpp new file mode 100644 index 0000000..4900f16 --- /dev/null +++ b/tests/postgres/test_offset.cpp @@ -0,0 +1,57 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_offset { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_offset) { + 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}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | order_by("id"_c) | offset(3); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_offset + +#endif diff --git a/tests/sqlite/test_offset.cpp b/tests/sqlite/test_offset.cpp new file mode 100644 index 0000000..7487ed0 --- /dev/null +++ b/tests/sqlite/test_offset.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_offset { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_offset) { + 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}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + // Note: SQLite does not support offset without limit. + const auto query = + sqlgen::read> | order_by("id"_c) | limit(5) | offset(3); + + const auto sql = sqlite::to_sql(query); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_offset