From 8ec3a4c1483084168d53ce899b7707dd1119d30d Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 25 Jun 2025 21:53:16 +0200 Subject: [PATCH 01/12] Added support for time stamps --- include/sqlgen/dynamic/TimeUnit.hpp | 20 ++++++++++++++++++++ include/sqlgen/dynamic/Value.hpp | 10 +++++++++- src/sqlgen/postgres/to_sql.cpp | 5 +++++ src/sqlgen/sqlite/to_sql.cpp | 5 +++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 include/sqlgen/dynamic/TimeUnit.hpp diff --git a/include/sqlgen/dynamic/TimeUnit.hpp b/include/sqlgen/dynamic/TimeUnit.hpp new file mode 100644 index 00000000..54f67bb0 --- /dev/null +++ b/include/sqlgen/dynamic/TimeUnit.hpp @@ -0,0 +1,20 @@ +#ifndef SQLGEN_DYNAMIC_TIMEUNIT_HPP_ +#define SQLGEN_DYNAMIC_TIMEUNIT_HPP_ + +namespace sqlgen::dynamic { + +enum class TimeUnit { + microseconds, + milliseconds, + seconds, + minutes, + hours, + days, + weeks, + months, + years +}; + +} + +#endif diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index c6b32594..6d692f08 100644 --- a/include/sqlgen/dynamic/Value.hpp +++ b/include/sqlgen/dynamic/Value.hpp @@ -4,8 +4,15 @@ #include #include +#include "TimeUnit.hpp" + namespace sqlgen::dynamic { +struct Duration { + TimeUnit unit; + int64_t val; +}; + struct Float { double val; }; @@ -19,7 +26,8 @@ struct String { }; struct Value { - using ReflectionType = rfl::TaggedUnion<"type", Float, Integer, String>; + using ReflectionType = + rfl::TaggedUnion<"type", Duration, Float, Integer, String>; const auto& reflection() const { return val; } ReflectionType val; }; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 7cf790d9..95443e9e 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -99,6 +99,11 @@ std::string column_or_value_to_sql( using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { return "'" + escape_single_quote(_v.val) + "'"; + + } else if constexpr (std::is_same_v) { + return "INTERVAL '" + std::to_string(_v.val) + " " + + rfl::enum_to_string(_v.unit) + "'"; + } else { return std::to_string(_v.val); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index befdf72b..f5148f03 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -83,6 +83,11 @@ std::string column_or_value_to_sql( using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { return "'" + escape_single_quote(_v.val) + "'"; + + } else if constexpr (std::is_same_v) { + /// TODO + return "TODO"; + } else { return std::to_string(_v.val); } From cc57739bd3e1c283ab9568b1d532a65106f08cef Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 26 Jun 2025 21:44:16 +0200 Subject: [PATCH 02/12] Redesigned time stamp operations --- include/sqlgen/dynamic/Operation.hpp | 13 ++++++++++--- include/sqlgen/dynamic/Value.hpp | 6 +++++- src/sqlgen/postgres/to_sql.cpp | 13 +++++++++++++ src/sqlgen/sqlite/to_sql.cpp | 18 ++++++++++++++++-- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 7360521e..65658726 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -8,6 +8,7 @@ #include "../Ref.hpp" #include "Column.hpp" +#include "ColumnOrValue.hpp" #include "Type.hpp" #include "Value.hpp" @@ -68,6 +69,11 @@ struct Operation { Ref op1; }; + struct DatePlusDuration { + ColumnOrValue date; + std::vector durations; + }; + struct Divides { Ref op1; Ref op2; @@ -161,9 +167,10 @@ struct Operation { using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Coalesce, - Concat, Cos, Divides, Exp, Floor, Length, Ln, Log2, - Lower, LTrim, Minus, Mod, Multiplies, Plus, Replace, - Round, RTrim, Sin, Sqrt, Tan, Trim, Upper, Value>; + Concat, Cos, DatePlusDuration, Divides, Exp, Floor, + Length, Ln, Log2, Lower, LTrim, Minus, Mod, Multiplies, + Plus, Replace, Round, RTrim, Sin, Sqrt, Tan, Trim, Upper, + Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index 6d692f08..f5cc9c06 100644 --- a/include/sqlgen/dynamic/Value.hpp +++ b/include/sqlgen/dynamic/Value.hpp @@ -25,9 +25,13 @@ struct String { std::string val; }; +struct Timestamp { + int64_t seconds_since_unix; +}; + struct Value { using ReflectionType = - rfl::TaggedUnion<"type", Duration, Float, Integer, String>; + rfl::TaggedUnion<"type", Duration, Float, Integer, String, Timestamp>; const auto& reflection() const { return val; } ReflectionType val; }; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 95443e9e..d55144fd 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -104,6 +104,9 @@ std::string column_or_value_to_sql( return "INTERVAL '" + std::to_string(_v.val) + " " + rfl::enum_to_string(_v.unit) + "'"; + } else if constexpr (std::is_same_v) { + return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")"; + } else { return std::to_string(_v.val); } @@ -407,6 +410,16 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_s.date) << " + " + << internal::strings::join( + " + ", + internal::collect::vector( + _s.durations | transform([](const auto& _d) { + return column_or_value_to_sql(dynamic::Value{_d}); + }))); + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") / (" << operation_to_sql(*_s.op2) << ")"; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index f5148f03..f72b7fc9 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -85,8 +85,11 @@ std::string column_or_value_to_sql( return "'" + escape_single_quote(_v.val) + "'"; } else if constexpr (std::is_same_v) { - /// TODO - return "TODO"; + return std::string("'") + (_v.val ? "+" : "-") + std::to_string(_v.val) + + " " + rfl::enum_to_string(_v.unit) + "'"; + + } else if constexpr (std::is_same_v) { + return std::to_string(_v.seconds_since_unix); } else { return std::to_string(_v.val); @@ -373,6 +376,17 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "unixepoch(" << column_or_value_to_sql(_s.date) << ", " + << internal::strings::join( + ", ", + internal::collect::vector( + _s.durations | transform([](const auto& _d) { + return column_or_value_to_sql(dynamic::Value{_d}); + }))) + << ")"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") / (" << operation_to_sql(*_s.op2) << ")"; From 1814af9aa3965d181c0f5e35576c2c1fb13694af Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sat, 28 Jun 2025 16:20:44 +0200 Subject: [PATCH 03/12] Added the ability to produce where statements with timestamps --- include/sqlgen/col.hpp | 25 +++++-- include/sqlgen/transpilation/Operator.hpp | 1 + .../transpilation/dynamic_operator_t.hpp | 7 ++ include/sqlgen/transpilation/is_duration.hpp | 23 +++++++ include/sqlgen/transpilation/make_field.hpp | 25 +++++++ include/sqlgen/transpilation/to_duration.hpp | 66 +++++++++++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 6 ++ tests/postgres/test_where_with_timestamps.cpp | 63 ++++++++++++++++++ 8 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 include/sqlgen/transpilation/is_duration.hpp create mode 100644 include/sqlgen/transpilation/to_duration.hpp create mode 100644 tests/postgres/test_where_with_timestamps.cpp diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index 5be2e4cd..25a0169e 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_COL_HPP_ #define SQLGEN_COL_HPP_ +#include #include #include @@ -13,6 +14,7 @@ #include "transpilation/Set.hpp" #include "transpilation/Value.hpp" #include "transpilation/conditions.hpp" +#include "transpilation/is_duration.hpp" #include "transpilation/to_transpilation_type.hpp" namespace sqlgen { @@ -169,13 +171,22 @@ struct Col { template friend auto operator+(const Col&, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; - - return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, - .operand2 = transpilation::to_transpilation_type(_op2)}; + if constexpr (transpilation::is_duration_v) { + return transpilation::Operation< + transpilation::Operator::date_plus_duration, + transpilation::Col<_name>, rfl::Tuple>>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = rfl::Tuple>(_op2)}; + + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return transpilation::Operation, OtherType>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = transpilation::to_transpilation_type(_op2)}; + } } }; diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index b6a037ed..467791c9 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -10,6 +10,7 @@ enum class Operator { coalesce, concat, cos, + date_plus_duration, divides, exp, floor, diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 95c696ee..62926c4f 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -54,6 +54,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Cos; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = std::numeric_limits::max(); + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::DatePlusDuration; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; diff --git a/include/sqlgen/transpilation/is_duration.hpp b/include/sqlgen/transpilation/is_duration.hpp new file mode 100644 index 00000000..0837e4d4 --- /dev/null +++ b/include/sqlgen/transpilation/is_duration.hpp @@ -0,0 +1,23 @@ +#ifndef SQLGEN_TRANSPILATION_IS_DURATION_HPP_ +#define SQLGEN_TRANSPILATION_IS_DURATION_HPP_ + +#include + +namespace sqlgen::transpilation { + +template +class is_duration; + +template +class is_duration : public std::false_type {}; + +template +class is_duration> : public std::true_type { +}; + +template +constexpr bool is_duration_v = is_duration>(); + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index c1bef43d..15dc5932 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -21,6 +21,7 @@ #include "dynamic_operator_t.hpp" #include "remove_as_t.hpp" #include "remove_nullable_t.hpp" +#include "to_duration.hpp" #include "to_value.hpp" #include "underlying_t.hpp" @@ -247,6 +248,30 @@ struct MakeField>> { } }; +template +struct MakeField, + rfl::Tuple>> { + 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 Operands = rfl::Tuple, DurationTypes...>; + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::DatePlusDuration{ + .date = dynamic::Column{.name = _name.str()}, + .durations = rfl::apply( + [](const auto&... _ops) { + return std::vector({to_duration(_ops)...}); + }, + _o.operand2)}}}; + } +}; + template struct MakeField +#include +#include + +#include "../dynamic/TimeUnit.hpp" +#include "../dynamic/Value.hpp" + +namespace sqlgen::transpilation { + +template +struct ToDuration { + dynamic::Duration operator()(const DurationType& _t) { + if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::microseconds, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::milliseconds, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::seconds, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::minutes, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::hours, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::days, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::weeks, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::months, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::years, + .val = static_cast(_t.count())}; + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + } +}; + +template +auto to_duration(const T& _t) { + return ToDuration>{}(_t); +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 1afa3ffb..b41b98d6 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -81,6 +81,12 @@ struct Underlying>> { std::optional, std::string>; }; +template +struct Underlying, + rfl::Tuple>> { + using Type = typename Underlying, Col<_name>>::Type; +}; + template struct Underlying< T, Operation> { diff --git a/tests/postgres/test_where_with_timestamps.cpp b/tests/postgres/test_where_with_timestamps.cpp new file mode 100644 index 00000000..64acb2ab --- /dev/null +++ b/tests/postgres/test_where_with_timestamps.cpp @@ -0,0 +1,63 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Timestamp<"%Y-%m-%d"> birthday; +}; + +TEST(postgres, test_where_with_timestamps) { + const auto people1 = std::vector( + {Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | + where("birthday"_c + std::chrono::years(11) > "2010-01-01") | + order_by("id"_c); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":2,"first_name":"Bart","last_name":"Simpson","birthday":"2000-01-01"},{"id":3,"first_name":"Lisa","last_name":"Simpson","birthday":"2002-01-01"},{"id":4,"first_name":"Maggie","last_name":"Simpson","birthday":"2010-01-01"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_timestamps + +#endif From 68aa7f4741c28c1a399e38d0500b23f0c3585b40 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sat, 28 Jun 2025 21:29:34 +0200 Subject: [PATCH 04/12] Make sure it also works with sqlite --- include/sqlgen/Timestamp.hpp | 3 + include/sqlgen/col.hpp | 25 ++++--- include/sqlgen/dynamic/TimeUnit.hpp | 1 - include/sqlgen/transpilation/Operation.hpp | 41 ++++++++---- include/sqlgen/transpilation/to_duration.hpp | 7 +- src/sqlgen/sqlite/to_sql.cpp | 34 +++++++++- .../test_select_from_with_timestamps.cpp | 66 +++++++++++++++++++ tests/postgres/test_where_with_timestamps.cpp | 33 +++++----- .../test_select_from_with_timestamps.cpp | 61 +++++++++++++++++ tests/sqlite/test_where_with_timestamps.cpp | 59 +++++++++++++++++ 10 files changed, 285 insertions(+), 45 deletions(-) create mode 100644 tests/postgres/test_select_from_with_timestamps.cpp create mode 100644 tests/sqlite/test_select_from_with_timestamps.cpp create mode 100644 tests/sqlite/test_where_with_timestamps.cpp diff --git a/include/sqlgen/Timestamp.hpp b/include/sqlgen/Timestamp.hpp index e21ed0df..e44be836 100644 --- a/include/sqlgen/Timestamp.hpp +++ b/include/sqlgen/Timestamp.hpp @@ -8,6 +8,9 @@ namespace sqlgen { template using Timestamp = rfl::Timestamp<_format>; +using Date = Timestamp<"%Y-%m-%d">; +using DateTime = Timestamp<"%Y-%m-%d %H:%M:%S">; + }; // namespace sqlgen #endif diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index 25a0169e..e2ad4716 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -137,14 +137,20 @@ struct Col { } template - friend auto operator-(const Col&, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + friend auto operator-(const Col& _op1, const T& _op2) noexcept { + if constexpr (transpilation::is_duration_v) { + using DurationType = std::remove_cvref_t; + return _op1 + DurationType(_op2.count() * (-1)); - return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, - .operand2 = transpilation::to_transpilation_type(_op2)}; + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return transpilation::Operation, OtherType>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = transpilation::to_transpilation_type(_op2)}; + } } template @@ -172,11 +178,12 @@ struct Col { template friend auto operator+(const Col&, const T& _op2) noexcept { if constexpr (transpilation::is_duration_v) { + using DurationType = std::remove_cvref_t; return transpilation::Operation< transpilation::Operator::date_plus_duration, - transpilation::Col<_name>, rfl::Tuple>>{ + transpilation::Col<_name>, rfl::Tuple>{ .operand1 = transpilation::Col<_name>{}, - .operand2 = rfl::Tuple>(_op2)}; + .operand2 = rfl::Tuple(_op2)}; } else { using OtherType = typename transpilation::ToTranspilationType< diff --git a/include/sqlgen/dynamic/TimeUnit.hpp b/include/sqlgen/dynamic/TimeUnit.hpp index 54f67bb0..ecaa571d 100644 --- a/include/sqlgen/dynamic/TimeUnit.hpp +++ b/include/sqlgen/dynamic/TimeUnit.hpp @@ -4,7 +4,6 @@ namespace sqlgen::dynamic { enum class TimeUnit { - microseconds, milliseconds, seconds, minutes, diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 887b9e9c..0a0cce21 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -9,6 +9,8 @@ #include "Condition.hpp" #include "Operator.hpp" #include "conditions.hpp" +#include "is_duration.hpp" +#include "to_duration.hpp" #include "to_transpilation_type.hpp" namespace sqlgen::transpilation { @@ -102,12 +104,18 @@ struct Operation { template friend auto operator-(const Operation& _op1, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + if constexpr (is_duration_v) { + using DurationType = std::remove_cvref_t; + return _op1 + DurationType(_op2.count() * (-1)); - return Operation, OtherType>{ - .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation, OtherType>{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } } template @@ -132,12 +140,23 @@ struct Operation { template friend auto operator+(const Operation& _op1, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; - - return Operation, OtherType>{ - .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + if constexpr (is_duration_v) { + using DurationType = std::remove_cvref_t; + const auto op2 = + rfl::tuple_cat(_op1.operand2, rfl::Tuple(_op2)); + using Op2Type = std::remove_cvref_t; + return transpilation::Operation< + transpilation::Operator::date_plus_duration, Operand1Type, Op2Type>{ + .operand1 = _op1.operand1, .operand2 = op2}; + + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation, OtherType>{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } } }; diff --git a/include/sqlgen/transpilation/to_duration.hpp b/include/sqlgen/transpilation/to_duration.hpp index 1746b03d..631f758b 100644 --- a/include/sqlgen/transpilation/to_duration.hpp +++ b/include/sqlgen/transpilation/to_duration.hpp @@ -13,12 +13,7 @@ namespace sqlgen::transpilation { template struct ToDuration { dynamic::Duration operator()(const DurationType& _t) { - if constexpr (std::is_same_v) { - return dynamic::Duration{.unit = dynamic::TimeUnit::microseconds, - .val = static_cast(_t.count())}; - - } else if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { return dynamic::Duration{.unit = dynamic::TimeUnit::milliseconds, .val = static_cast(_t.count())}; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index f72b7fc9..c1adb42e 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -77,6 +77,15 @@ std::string aggregation_to_sql( }); } +std::string pad_with_zeros(const std::string& _str, + const size_t _expected_length) { + if (_str.size() < _expected_length) { + return std::string(_expected_length - _str.size(), '0') + _str; + } else { + return _str; + } +} + std::string column_or_value_to_sql( const dynamic::ColumnOrValue& _col) noexcept { const auto handle_value = [](const auto& _v) -> std::string { @@ -85,8 +94,27 @@ std::string column_or_value_to_sql( return "'" + escape_single_quote(_v.val) + "'"; } else if constexpr (std::is_same_v) { - return std::string("'") + (_v.val ? "+" : "-") + std::to_string(_v.val) + - " " + rfl::enum_to_string(_v.unit) + "'"; + const auto prefix = std::string("'") + (_v.val >= 0 ? "+" : "-"); + const auto val = std::abs(_v.val); + switch (_v.unit) { + case dynamic::TimeUnit::milliseconds: { + const auto h = (val / 3600000); + const auto m = (val / 60000) % 60; + const auto s = (val / 1000) % 60; + const auto ms = val % 1000; + return prefix + pad_with_zeros(std::to_string(h), 2) + ":" + + pad_with_zeros(std::to_string(m), 2) + ":" + + pad_with_zeros(std::to_string(s), 2) + "." + + pad_with_zeros(std::to_string(ms), 3) + "'"; + } + + case dynamic::TimeUnit::weeks: + return prefix + std::to_string(val * 7) + " days'"; + + default: + return prefix + std::to_string(val) + " " + + rfl::enum_to_string(_v.unit) + "'"; + } } else if constexpr (std::is_same_v) { return std::to_string(_v.seconds_since_unix); @@ -378,7 +406,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { - stream << "unixepoch(" << column_or_value_to_sql(_s.date) << ", " + stream << "datetime(" << column_or_value_to_sql(_s.date) << ", " << internal::strings::join( ", ", internal::collect::vector( diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp new file mode 100644 index 00000000..09f1537c --- /dev/null +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Timestamp<"%Y-%m-%d"> birthday; +}; + +TEST(postgres, test_range_select_from_with_timestamps) { + const auto people1 = std::vector( + {Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + struct Birthday { + sqlgen::Timestamp<"%Y-%m-%d"> birthday; + }; + + const auto birthdays = + postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then( + select_from( + ("birthday"_c + std::chrono::days(10)).as<"birthday">()) | + order_by("id"_c) | to>) + .value(); + + const std::string expected = + R"([{"birthday":"1970-01-11"},{"birthday":"2000-01-11"},{"birthday":"2002-01-11"},{"birthday":"2010-01-11"}])"; + + EXPECT_EQ(rfl::json::write(birthdays), expected); +} + +} // namespace test_range_select_from_with_timestamps + +#endif diff --git a/tests/postgres/test_where_with_timestamps.cpp b/tests/postgres/test_where_with_timestamps.cpp index 64acb2ab..97b8591a 100644 --- a/tests/postgres/test_where_with_timestamps.cpp +++ b/tests/postgres/test_where_with_timestamps.cpp @@ -15,23 +15,23 @@ struct Person { sqlgen::PrimaryKey id; std::string first_name; std::string last_name; - sqlgen::Timestamp<"%Y-%m-%d"> birthday; + sqlgen::Date birthday; }; TEST(postgres, test_where_with_timestamps) { - const auto people1 = std::vector( - {Person{.first_name = "Homer", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, - Person{.first_name = "Bart", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, - Person{.first_name = "Lisa", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, - Person{.first_name = "Maggie", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", .password = "password", @@ -39,6 +39,7 @@ TEST(postgres, test_where_with_timestamps) { .dbname = "postgres"}; using namespace sqlgen; + using namespace std::literals::chrono_literals; const auto conn = sqlgen::postgres::connect(credentials).and_then(drop | if_exists); @@ -47,7 +48,9 @@ TEST(postgres, test_where_with_timestamps) { const auto query = sqlgen::read> | - where("birthday"_c + std::chrono::years(11) > "2010-01-01") | + where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + + std::chrono::milliseconds(4000000) > + "2010-01-01") | order_by("id"_c); const auto people2 = query(conn).value(); diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp new file mode 100644 index 00000000..5a2cb261 --- /dev/null +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -0,0 +1,61 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Timestamp<"%Y-%m-%d"> birthday; +}; + +TEST(sqlite, test_range_select_from_with_timestamps) { + const auto people1 = std::vector( + {Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + + using namespace sqlgen; + + struct Birthday { + sqlgen::Date birthday; + }; + + const auto query = + select_from( + ("birthday"_c + std::chrono::days(10)).as<"birthday">()) | + order_by("id"_c) | to>; + + const auto birthdays = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(query) + .value(); + + const std::string expected_query = + R"(SELECT datetime("birthday", '+10 days') AS "birthday" FROM "Person" ORDER BY "id";)"; + const std::string expected = + R"([{"birthday":"1970-01-11"},{"birthday":"2000-01-11"},{"birthday":"2002-01-11"},{"birthday":"2010-01-11"}])"; + + EXPECT_EQ(sqlite::to_sql(query), expected_query); + EXPECT_EQ(rfl::json::write(birthdays), expected); +} + +} // namespace test_range_select_from_with_timestamps + diff --git a/tests/sqlite/test_where_with_timestamps.cpp b/tests/sqlite/test_where_with_timestamps.cpp new file mode 100644 index 00000000..bf96cb93 --- /dev/null +++ b/tests/sqlite/test_where_with_timestamps.cpp @@ -0,0 +1,59 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(sqlite, test_where_with_timestamps) { + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); + + using namespace sqlgen; + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | + where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + + std::chrono::milliseconds(4000005) > + "2010-01-01") | + order_by("id"_c); + + const auto people2 = query(conn).value(); + + const std::string expected_query = + R"(SELECT "id", "first_name", "last_name", "birthday" FROM "Person" WHERE datetime("birthday", '+11 years', '-70 days', '+01:06:40.005') > '2010-01-01' ORDER BY "id";)"; + const std::string expected = + R"([{"id":2,"first_name":"Bart","last_name":"Simpson","birthday":"2000-01-01"},{"id":3,"first_name":"Lisa","last_name":"Simpson","birthday":"2002-01-01"},{"id":4,"first_name":"Maggie","last_name":"Simpson","birthday":"2010-01-01"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_timestamps + From 28845e619f12d1f16db58938f385e39b766df3e6 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sat, 28 Jun 2025 23:45:43 +0200 Subject: [PATCH 05/12] Added more type safety --- include/sqlgen/transpilation/is_timestamp.hpp | 25 ++++++++++ include/sqlgen/transpilation/make_field.hpp | 3 ++ .../transpilation/remove_reflection_t.hpp | 13 +++-- include/sqlgen/transpilation/to_condition.hpp | 49 ++++++++++++++----- include/sqlgen/transpilation/underlying_t.hpp | 2 +- .../test_select_from_with_timestamps.cpp | 2 +- tests/postgres/test_where_with_timestamps.cpp | 2 +- .../test_select_from_with_timestamps.cpp | 30 ++++++------ tests/sqlite/test_where_with_timestamps.cpp | 2 +- 9 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 include/sqlgen/transpilation/is_timestamp.hpp diff --git a/include/sqlgen/transpilation/is_timestamp.hpp b/include/sqlgen/transpilation/is_timestamp.hpp new file mode 100644 index 00000000..01120924 --- /dev/null +++ b/include/sqlgen/transpilation/is_timestamp.hpp @@ -0,0 +1,25 @@ +#ifndef SQLGEN_TRANSPILATION_IS_TIMESTAMP_HPP_ +#define SQLGEN_TRANSPILATION_IS_TIMESTAMP_HPP_ + +#include +#include + +#include "../PrimaryKey.hpp" + +namespace sqlgen::transpilation { + +template +class is_timestamp; + +template +class is_timestamp : public std::false_type {}; + +template +class is_timestamp> : public std::true_type {}; + +template +constexpr bool is_timestamp_v = is_timestamp>::value; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 15dc5932..774d643b 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -19,6 +19,7 @@ #include "all_columns_exist.hpp" #include "dynamic_aggregation_t.hpp" #include "dynamic_operator_t.hpp" +#include "is_timestamp.hpp" #include "remove_as_t.hpp" #include "remove_nullable_t.hpp" #include "to_duration.hpp" @@ -260,6 +261,8 @@ struct MakeField, using Type = underlying_t>; using Operands = rfl::Tuple, DurationTypes...>; + static_assert(is_timestamp_v, "Must be a timestamp."); + dynamic::SelectFrom::Field operator()(const auto& _o) const { return dynamic::SelectFrom::Field{ dynamic::Operation{dynamic::Operation::DatePlusDuration{ diff --git a/include/sqlgen/transpilation/remove_reflection_t.hpp b/include/sqlgen/transpilation/remove_reflection_t.hpp index 1c37da50..7fcb585d 100644 --- a/include/sqlgen/transpilation/remove_reflection_t.hpp +++ b/include/sqlgen/transpilation/remove_reflection_t.hpp @@ -13,15 +13,20 @@ struct RemoveReflection { using Type = T; }; +template +struct RemoveReflection> { + using Type = rfl::Timestamp<_format>; +}; + template - requires has_reflection_method> + requires has_reflection_method struct RemoveReflection { - using Type = typename RemoveReflection< - typename std::remove_cvref_t::ReflectionType>::Type; + using Type = typename RemoveReflection::Type; }; template -using remove_reflection_t = typename RemoveReflection::Type; +using remove_reflection_t = + typename RemoveReflection>::Type; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/to_condition.hpp b/include/sqlgen/transpilation/to_condition.hpp index 113b77c5..3b6ed7a5 100644 --- a/include/sqlgen/transpilation/to_condition.hpp +++ b/include/sqlgen/transpilation/to_condition.hpp @@ -12,6 +12,7 @@ #include "Condition.hpp" #include "all_columns_exist.hpp" #include "conditions.hpp" +#include "is_timestamp.hpp" #include "make_field.hpp" #include "to_transpilation_type.hpp" #include "underlying_t.hpp" @@ -43,8 +44,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::equality_comparable_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::equality_comparable_with || + (is_timestamp_v && + is_timestamp_v), "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { @@ -56,8 +61,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -69,8 +78,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -82,8 +95,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -95,8 +112,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -148,8 +169,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::equality_comparable_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::equality_comparable_with || + (is_timestamp_v && + is_timestamp_v), "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index b41b98d6..1fe87646 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -202,7 +202,7 @@ struct Underlying> { template struct Underlying> { - using Type = _Type; + using Type = remove_reflection_t<_Type>; }; template diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp index 09f1537c..4e58db37 100644 --- a/tests/postgres/test_select_from_with_timestamps.cpp +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -42,7 +42,7 @@ TEST(postgres, test_range_select_from_with_timestamps) { using namespace sqlgen; struct Birthday { - sqlgen::Timestamp<"%Y-%m-%d"> birthday; + Date birthday; }; const auto birthdays = diff --git a/tests/postgres/test_where_with_timestamps.cpp b/tests/postgres/test_where_with_timestamps.cpp index 97b8591a..98758842 100644 --- a/tests/postgres/test_where_with_timestamps.cpp +++ b/tests/postgres/test_where_with_timestamps.cpp @@ -50,7 +50,7 @@ TEST(postgres, test_where_with_timestamps) { sqlgen::read> | where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + std::chrono::milliseconds(4000000) > - "2010-01-01") | + Date("2010-01-01")) | order_by("id"_c); const auto people2 = query(conn).value(); diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp index 5a2cb261..1e135356 100644 --- a/tests/sqlite/test_select_from_with_timestamps.cpp +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -14,28 +14,28 @@ struct Person { sqlgen::PrimaryKey id; std::string first_name; std::string last_name; - sqlgen::Timestamp<"%Y-%m-%d"> birthday; + sqlgen::Date birthday; }; TEST(sqlite, test_range_select_from_with_timestamps) { - const auto people1 = std::vector( - {Person{.first_name = "Homer", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, - Person{.first_name = "Bart", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, - Person{.first_name = "Lisa", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, - Person{.first_name = "Maggie", - .last_name = "Simpson", - .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); using namespace sqlgen; struct Birthday { - sqlgen::Date birthday; + Date birthday; }; const auto query = diff --git a/tests/sqlite/test_where_with_timestamps.cpp b/tests/sqlite/test_where_with_timestamps.cpp index bf96cb93..066492d0 100644 --- a/tests/sqlite/test_where_with_timestamps.cpp +++ b/tests/sqlite/test_where_with_timestamps.cpp @@ -42,7 +42,7 @@ TEST(sqlite, test_where_with_timestamps) { sqlgen::read> | where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + std::chrono::milliseconds(4000005) > - "2010-01-01") | + Date("2010-01-01")) | order_by("id"_c); const auto people2 = query(conn).value(); From 3df1202d0e0d38bb44fd250d9092065bdb62534d Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 1 Jul 2025 20:33:48 +0200 Subject: [PATCH 06/12] Added days_between and unixepoch --- include/sqlgen/dynamic/Operation.hpp | 17 +- include/sqlgen/operations.hpp | 20 +++ include/sqlgen/transpilation/Operator.hpp | 2 + .../transpilation/dynamic_operator_t.hpp | 14 ++ include/sqlgen/transpilation/make_field.hpp | 157 +++++------------- include/sqlgen/transpilation/underlying_t.hpp | 15 ++ src/sqlgen/postgres/to_sql.cpp | 8 + src/sqlgen/sqlite/to_sql.cpp | 8 + .../test_select_from_with_timestamps.cpp | 26 +-- .../test_select_from_with_timestamps.cpp | 17 +- 10 files changed, 147 insertions(+), 137 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 65658726..541a21ba 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -74,6 +74,11 @@ struct Operation { std::vector durations; }; + struct DaysBetween { + Ref op1; + Ref op2; + }; + struct Divides { Ref op1; Ref op2; @@ -161,16 +166,20 @@ struct Operation { Ref op2; }; + struct Unixepoch { + Ref op1; + }; + struct Upper { Ref op1; }; using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Coalesce, - Concat, Cos, DatePlusDuration, Divides, Exp, Floor, - Length, Ln, Log2, Lower, LTrim, Minus, Mod, Multiplies, - Plus, Replace, Round, RTrim, Sin, Sqrt, Tan, Trim, Upper, - Value>; + Concat, Cos, DatePlusDuration, DaysBetween, Divides, Exp, + Floor, Length, Ln, Log2, Lower, LTrim, Minus, Mod, + Multiplies, Plus, Replace, Round, RTrim, Sin, Sqrt, Tan, + Trim, Unixepoch, Upper, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index ca8c01a5..a94db6a2 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -65,6 +65,18 @@ auto cos(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto days_between(const T& _t, const U& _u) { + using Type1 = + typename transpilation::ToTranspilationType>::Type; + using Type2 = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t), + .operand2 = transpilation::to_transpilation_type(_u)}; +} + template auto exp(const T& _t) { using Type = @@ -216,6 +228,14 @@ auto trim(const T& _t) { return trim(_t, std::string(" ")); } +template +auto unixepoch(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto upper(const T& _t) { using Type = diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 467791c9..528a9c39 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -11,6 +11,7 @@ enum class Operator { concat, cos, date_plus_duration, + days_between, divides, exp, floor, @@ -30,6 +31,7 @@ enum class Operator { sqrt, tan, trim, + unixepoch, upper }; diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 62926c4f..c88937ed 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -61,6 +61,13 @@ struct DynamicOperator { using Type = dynamic::Operation::DatePlusDuration; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::DaysBetween; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -194,6 +201,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Trim; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Unixepoch; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 774d643b..fafde6c5 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_TRANSPILATION_MAKE_FIELD_HPP_ #define SQLGEN_TRANSPILATION_MAKE_FIELD_HPP_ +#include #include #include @@ -222,33 +223,6 @@ struct MakeField - requires((_op == Operator::coalesce) || (_op == Operator::concat)) -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 Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - using DynamicOperatorType = dynamic_operator_t<_op>; - return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .ops = rfl::apply( - [](const auto&... _ops) { - return std::vector>( - {Ref::make( - MakeField>{}(_ops) - .val)...}); - }, - _o.operand1)}}}; - } -}; - template struct MakeField, @@ -275,68 +249,8 @@ struct MakeField, } }; -template -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 Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{dynamic::Operation::Replace{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val), - .op3 = Ref::make( - MakeField>{}( - _o.operand3) - .val)}}}; - } -}; - -template -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 Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{dynamic::Operation::Round{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; - } -}; - template - requires((num_operands_v<_op>) == 1 && - (operator_category_v<_op>) == OperatorCategory::string) + requires((num_operands_v<_op>) == 1) struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; @@ -358,8 +272,7 @@ struct MakeField> { template - requires((num_operands_v<_op>) == 2 && - (operator_category_v<_op>) == OperatorCategory::string) + requires((num_operands_v<_op>) == 2) struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; @@ -384,53 +297,63 @@ struct MakeField> { } }; -template - requires((num_operands_v<_op>) == 1 && - (operator_category_v<_op>) == OperatorCategory::numerical) -struct MakeField> { +template + requires((num_operands_v<_op>) == 3) +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 Operands = rfl::Tuple; + 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>{}( - _o.operand1) - .val)}}}; + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::Replace{ + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .op2 = Ref::make( + MakeField>{}( + _o.operand2) + .val), + .op3 = Ref::make( + MakeField>{}( + _o.operand3) + .val)}}}; } }; -template - requires((num_operands_v<_op>) == 2 && - (operator_category_v<_op>) == OperatorCategory::numerical) -struct MakeField> { +template + requires((num_operands_v<_op>) == std::numeric_limits::max()) +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 Operands = rfl::Tuple; + 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>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; + .ops = rfl::apply( + [](const auto&... _ops) { + return std::vector>( + {Ref::make( + MakeField>{}(_ops) + .val)...}); + }, + _o.operand1)}}}; } }; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 1fe87646..5f7441d5 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -14,6 +14,7 @@ #include "all_columns_exist.hpp" #include "dynamic_operator_t.hpp" #include "is_nullable.hpp" +#include "is_timestamp.hpp" #include "remove_nullable_t.hpp" #include "remove_reflection_t.hpp" @@ -85,6 +86,13 @@ template struct Underlying, rfl::Tuple>> { using Type = typename Underlying, Col<_name>>::Type; + static_assert(is_timestamp_v, "Must be a timestamp"); +}; + +template +struct Underlying< + T, Operation> { + using Type = double; }; template @@ -125,6 +133,13 @@ struct Underlying> { using Type = Underlying1; }; +template +struct Underlying> { + static_assert(is_timestamp_v::Type>, + "Must be a timestamp"); + using Type = time_t; +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::string) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index d55144fd..340c6363 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -420,6 +420,11 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return column_or_value_to_sql(dynamic::Value{_d}); }))); + } else if constexpr (std::is_same_v) { + stream << "cast(" << operation_to_sql(*_s.op2) << " as DATE) - cast(" + << operation_to_sql(*_s.op1) << " as DATE)"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") / (" << operation_to_sql(*_s.op2) << ")"; @@ -488,6 +493,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "trim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(EPOCH FROM" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "upper(" << operation_to_sql(*_s.op1) << ")"; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index c1adb42e..5ee368ee 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -404,6 +404,11 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "julianday(" << operation_to_sql(*_s.op2) << ") - julianday(" + << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "datetime(" << column_or_value_to_sql(_s.date) << ", " @@ -483,6 +488,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "trim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "unixepoch(" << operation_to_sql(*_s.op1) << ", 'subsec')"; + } else if constexpr (std::is_same_v) { stream << "upper(" << operation_to_sql(*_s.op1) << ")"; diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp index 4e58db37..49262a8a 100644 --- a/tests/postgres/test_select_from_with_timestamps.cpp +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -43,20 +43,26 @@ TEST(postgres, test_range_select_from_with_timestamps) { struct Birthday { Date birthday; + time_t birthday_unixepoch; + double age_in_days; }; - const auto birthdays = - postgres::connect(credentials) - .and_then(drop | if_exists) - .and_then(write(std::ref(people1))) - .and_then( - select_from( - ("birthday"_c + std::chrono::days(10)).as<"birthday">()) | - order_by("id"_c) | to>) - .value(); + const auto get_birthdays = + select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | + as<"birthday_unixepoch">) | + order_by("id"_c) | to>; + + const auto birthdays = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_birthdays) + .value(); const std::string expected = - R"([{"birthday":"1970-01-11"},{"birthday":"2000-01-11"},{"birthday":"2002-01-11"},{"birthday":"2010-01-11"}])"; + R"([{"birthday":"1970-01-11","birthday_unixepoch":864000,"age_in_days":14975.0},{"birthday":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0},{"birthday":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0},{"birthday":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0}])"; EXPECT_EQ(rfl::json::write(birthdays), expected); } diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp index 1e135356..ae965c98 100644 --- a/tests/sqlite/test_select_from_with_timestamps.cpp +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -36,24 +36,29 @@ TEST(sqlite, test_range_select_from_with_timestamps) { struct Birthday { Date birthday; + time_t birthday_unixepoch; + double age_in_days; }; - const auto query = + const auto get_birthdays = select_from( - ("birthday"_c + std::chrono::days(10)).as<"birthday">()) | + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | + as<"birthday_unixepoch">) | order_by("id"_c) | to>; const auto birthdays = sqlite::connect() .and_then(write(std::ref(people1))) - .and_then(query) + .and_then(get_birthdays) .value(); const std::string expected_query = - R"(SELECT datetime("birthday", '+10 days') AS "birthday" FROM "Person" ORDER BY "id";)"; + R"(SELECT datetime("birthday", '+10 days') AS "birthday", julianday('2011-01-01') - julianday("birthday") AS "age_in_days", unixepoch(datetime("birthday", '+10 days'), 'subsec') AS "birthday_unixepoch" FROM "Person" ORDER BY "id";)"; const std::string expected = - R"([{"birthday":"1970-01-11"},{"birthday":"2000-01-11"},{"birthday":"2002-01-11"},{"birthday":"2010-01-11"}])"; + R"([{"birthday":"1970-01-11","birthday_unixepoch":864000,"age_in_days":14975.0},{"birthday":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0},{"birthday":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0},{"birthday":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0}])"; - EXPECT_EQ(sqlite::to_sql(query), expected_query); + EXPECT_EQ(sqlite::to_sql(get_birthdays), expected_query); EXPECT_EQ(rfl::json::write(birthdays), expected); } From f8e2922b0149a3552e0766958997d9445afbf14c Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 1 Jul 2025 20:49:29 +0200 Subject: [PATCH 07/12] Allow operations inside DatePlusDuration --- include/sqlgen/dynamic/Operation.hpp | 2 +- include/sqlgen/transpilation/make_field.hpp | 17 ++++++++++------- src/sqlgen/postgres/to_sql.cpp | 2 +- src/sqlgen/sqlite/to_sql.cpp | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 541a21ba..4644f58e 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -70,7 +70,7 @@ struct Operation { }; struct DatePlusDuration { - ColumnOrValue date; + Ref date; std::vector durations; }; diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index fafde6c5..f422270d 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -223,24 +223,27 @@ struct MakeField -struct MakeField, - rfl::Tuple>> { +template +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 Operands = rfl::Tuple, DurationTypes...>; + using Type = underlying_t>; + using Operands = rfl::Tuple; static_assert(is_timestamp_v, "Must be a timestamp."); dynamic::SelectFrom::Field operator()(const auto& _o) const { return dynamic::SelectFrom::Field{ dynamic::Operation{dynamic::Operation::DatePlusDuration{ - .date = dynamic::Column{.name = _name.str()}, + .date = Ref::make( + MakeField>{}( + _o.operand1) + .val), .durations = rfl::apply( [](const auto&... _ops) { return std::vector({to_duration(_ops)...}); diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 340c6363..91d77179 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -412,7 +412,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_s.date) << " + " + stream << operation_to_sql(*_s.date) << " + " << internal::strings::join( " + ", internal::collect::vector( diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 5ee368ee..f60190ec 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -411,7 +411,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { - stream << "datetime(" << column_or_value_to_sql(_s.date) << ", " + stream << "datetime(" << operation_to_sql(*_s.date) << ", " << internal::strings::join( ", ", internal::collect::vector( From e4277be3735054d1647fdaa8c1bb75bfedd59705 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 1 Jul 2025 22:12:11 +0200 Subject: [PATCH 08/12] Added date_part operators --- include/sqlgen/dynamic/Operation.hpp | 37 ++++++++++-- include/sqlgen/operations.hpp | 56 +++++++++++++++++++ include/sqlgen/transpilation/Operator.hpp | 9 ++- .../sqlgen/transpilation/OperatorCategory.hpp | 2 +- .../transpilation/dynamic_operator_t.hpp | 49 ++++++++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 48 +++++++++++++--- src/sqlgen/postgres/to_sql.cpp | 21 +++++++ src/sqlgen/sqlite/to_sql.cpp | 28 ++++++++++ .../test_select_from_with_timestamps.cpp | 19 ++++++- .../test_select_from_with_timestamps.cpp | 18 +++++- 10 files changed, 269 insertions(+), 18 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 4644f58e..281f2702 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -74,6 +74,10 @@ struct Operation { std::vector durations; }; + struct Day { + Ref op1; + }; + struct DaysBetween { Ref op1; Ref op2; @@ -92,6 +96,10 @@ struct Operation { Ref op1; }; + struct Hour { + Ref op1; + }; + struct Length { Ref op1; }; @@ -118,11 +126,19 @@ struct Operation { Ref op2; }; + struct Minute { + Ref op1; + }; + struct Mod { Ref op1; Ref op2; }; + struct Month { + Ref op1; + }; + struct Multiplies { Ref op1; Ref op2; @@ -149,6 +165,10 @@ struct Operation { Ref op2; }; + struct Second { + Ref op1; + }; + struct Sin { Ref op1; }; @@ -174,12 +194,21 @@ struct Operation { Ref op1; }; + struct Weekday { + Ref op1; + }; + + struct Year { + Ref op1; + }; + using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Coalesce, - Concat, Cos, DatePlusDuration, DaysBetween, Divides, Exp, - Floor, Length, Ln, Log2, Lower, LTrim, Minus, Mod, - Multiplies, Plus, Replace, Round, RTrim, Sin, Sqrt, Tan, - Trim, Unixepoch, Upper, Value>; + Concat, Cos, DatePlusDuration, Day, DaysBetween, Divides, + Exp, Floor, Hour, Length, Ln, Log2, Lower, LTrim, Month, + Minus, Minute, Mod, Multiplies, Plus, Replace, Round, + RTrim, Second, Sin, Sqrt, Tan, Trim, Unixepoch, Upper, + Value, Weekday, Year>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index a94db6a2..c4081eda 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -65,6 +65,14 @@ auto cos(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto day(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto days_between(const T& _t, const U& _u) { using Type1 = @@ -93,6 +101,14 @@ auto floor(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto hour(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto length(const T& _t) { using Type = @@ -141,6 +157,22 @@ auto ltrim(const T& _t) { return ltrim(_t, std::string(" ")); } +template +auto minute(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto month(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto replace(const StringType& _str, const FromType& _from, const ToType& _to) { using Type1 = typename transpilation::ToTranspilationType< @@ -188,6 +220,14 @@ auto rtrim(const T& _t) { return rtrim(_t, std::string(" ")); } +template +auto second(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto sin(const T& _t) { using Type = @@ -244,6 +284,22 @@ auto upper(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto weekday(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto year(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + } // namespace sqlgen #endif diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 528a9c39..9bcc6d91 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -11,28 +11,35 @@ enum class Operator { concat, cos, date_plus_duration, + day, days_between, divides, exp, floor, + hour, length, ln, log2, lower, ltrim, minus, + minute, mod, + month, multiplies, plus, replace, round, rtrim, + second, sin, sqrt, tan, trim, unixepoch, - upper + upper, + weekday, + year }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/OperatorCategory.hpp b/include/sqlgen/transpilation/OperatorCategory.hpp index 3092d82b..a3f1af4d 100644 --- a/include/sqlgen/transpilation/OperatorCategory.hpp +++ b/include/sqlgen/transpilation/OperatorCategory.hpp @@ -3,7 +3,7 @@ namespace sqlgen::transpilation { -enum class OperatorCategory { numerical, string, other }; +enum class OperatorCategory { date_part, numerical, string, other }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index c88937ed..8b4f7632 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -61,6 +61,13 @@ struct DynamicOperator { using Type = dynamic::Operation::DatePlusDuration; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Day; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -89,6 +96,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Floor; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Hour; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -131,6 +145,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Minus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Minute; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -138,6 +159,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Mod; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Month; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -173,6 +201,13 @@ struct DynamicOperator { using Type = dynamic::Operation::RTrim; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Second; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -215,6 +250,20 @@ struct DynamicOperator { using Type = dynamic::Operation::Upper; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Weekday; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Year; +}; + template using dynamic_operator_t = typename DynamicOperator::Type; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 5f7441d5..213c3e9a 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -82,17 +82,33 @@ struct Underlying>> { std::optional, std::string>; }; -template -struct Underlying, +template +struct Underlying>> { - using Type = typename Underlying, Col<_name>>::Type; - static_assert(is_timestamp_v, "Must be a timestamp"); + using Underlying1 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t, + std::optional>, + Underlying1>; }; template struct Underlying< T, Operation> { - using Type = double; + using Underlying1 = typename Underlying::Type; + using Underlying2 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t || + is_nullable_v, + std::optional, double>; }; template @@ -135,9 +151,27 @@ struct Underlying> { template struct Underlying> { - static_assert(is_timestamp_v::Type>, + using Underlying1 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t, + std::optional, time_t>; +}; + +template + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::date_part) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + + static_assert(is_timestamp_v>, "Must be a timestamp"); - using Type = time_t; + + using Type = + std::conditional_t, std::optional, int>; }; template diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 91d77179..7f7460ff 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -420,6 +420,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return column_or_value_to_sql(dynamic::Value{_d}); }))); + } else if constexpr (std::is_same_v) { + stream << "extract(DAY from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "cast(" << operation_to_sql(*_s.op2) << " as DATE) - cast(" @@ -435,6 +438,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "floor(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(HOUR from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "length(" << operation_to_sql(*_s.op1) << ")"; @@ -455,10 +461,16 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "(" << operation_to_sql(*_s.op1) << ") - (" << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(MINUTE from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "mod(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(MONTH from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") * (" << operation_to_sql(*_s.op2) << ")"; @@ -480,6 +492,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(SECOND from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "sin(" << operation_to_sql(*_s.op1) << ")"; @@ -502,6 +517,12 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + stream << "extract(DOW from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(YEAR from " << operation_to_sql(*_s.op1) << ")"; + } else { static_assert(rfl::always_false_v, "Unsupported type."); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index f60190ec..3a76fca8 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -404,6 +404,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%d', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "julianday(" << operation_to_sql(*_s.op2) << ") - julianday(" @@ -430,6 +434,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "floor(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%H', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "length(" << operation_to_sql(*_s.op1) << ")"; @@ -450,10 +458,18 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "(" << operation_to_sql(*_s.op1) << ") - (" << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%M', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "mod(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%m', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") * (" << operation_to_sql(*_s.op2) << ")"; @@ -475,6 +491,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%S', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "sin(" << operation_to_sql(*_s.op1) << ")"; @@ -497,6 +517,14 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%w', " << operation_to_sql(*_s.op1) + << ") as INT)"; + + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%Y', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else { static_assert(rfl::always_false_v, "Unsupported type."); } diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp index 49262a8a..889e14e7 100644 --- a/tests/postgres/test_select_from_with_timestamps.cpp +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -43,16 +43,28 @@ TEST(postgres, test_range_select_from_with_timestamps) { struct Birthday { Date birthday; + Date birthday_recreated; time_t birthday_unixepoch; double age_in_days; + int hour; + int minute; + int second; + int weekday; }; const auto get_birthdays = select_from( ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c)))) | + as<"birthday_recreated">, days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, unixepoch("birthday"_c + std::chrono::days(10)) | - as<"birthday_unixepoch">) | + as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday">) | order_by("id"_c) | to>; const auto birthdays = postgres::connect(credentials) @@ -61,9 +73,12 @@ TEST(postgres, test_range_select_from_with_timestamps) { .and_then(get_birthdays) .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) 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_unixepoch":864000,"age_in_days":14975.0},{"birthday":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0},{"birthday":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0},{"birthday":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0}])"; + 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}])"; + EXPECT_EQ(postgres::to_sql(get_birthdays), expected_query); EXPECT_EQ(rfl::json::write(birthdays), expected); } diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp index ae965c98..1fbc16b2 100644 --- a/tests/sqlite/test_select_from_with_timestamps.cpp +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -36,16 +36,28 @@ TEST(sqlite, test_range_select_from_with_timestamps) { struct Birthday { Date birthday; + Date birthday_recreated; time_t birthday_unixepoch; double age_in_days; + int hour; + int minute; + int second; + int weekday; }; const auto get_birthdays = select_from( ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c)))) | + as<"birthday_recreated">, days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, unixepoch("birthday"_c + std::chrono::days(10)) | - as<"birthday_unixepoch">) | + as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday">) | order_by("id"_c) | to>; const auto birthdays = sqlite::connect() @@ -54,9 +66,9 @@ TEST(sqlite, test_range_select_from_with_timestamps) { .value(); const std::string expected_query = - R"(SELECT datetime("birthday", '+10 days') AS "birthday", julianday('2011-01-01') - julianday("birthday") AS "age_in_days", unixepoch(datetime("birthday", '+10 days'), 'subsec') AS "birthday_unixepoch" 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_unixepoch":864000,"age_in_days":14975.0},{"birthday":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0},{"birthday":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0},{"birthday":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0}])"; + 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}])"; EXPECT_EQ(sqlite::to_sql(get_birthdays), expected_query); EXPECT_EQ(rfl::json::write(birthdays), expected); From 632b45c61e772ed0a42a44aa459601a006825d05 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 2 Jul 2025 19:23:14 +0200 Subject: [PATCH 09/12] Made sure that date_plus_duration can be used on other operations --- include/sqlgen/transpilation/Operation.hpp | 37 ++++++++++--------- .../test_select_from_with_timestamps.cpp | 12 +++--- .../test_select_from_with_timestamps.cpp | 6 +-- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 0a0cce21..46c51459 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -97,8 +97,7 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } @@ -112,8 +111,7 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } } @@ -123,8 +121,7 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } @@ -133,28 +130,34 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } template friend auto operator+(const Operation& _op1, const T& _op2) noexcept { if constexpr (is_duration_v) { - using DurationType = std::remove_cvref_t; - const auto op2 = - rfl::tuple_cat(_op1.operand2, rfl::Tuple(_op2)); - using Op2Type = std::remove_cvref_t; - return transpilation::Operation< - transpilation::Operator::date_plus_duration, Operand1Type, Op2Type>{ - .operand1 = _op1.operand1, .operand2 = op2}; + if constexpr (Operation::op == Operator::date_plus_duration) { + using DurationType = std::remove_cvref_t; + const auto op2 = + rfl::tuple_cat(_op1.operand2, rfl::Tuple(_op2)); + using Op2Type = std::remove_cvref_t; + return transpilation::Operation< + transpilation::Operator::date_plus_duration, Operand1Type, Op2Type>{ + .operand1 = _op1.operand1, .operand2 = op2}; + + } else { + using DurationType = std::remove_cvref_t; + return Operation>{ + .operand1 = _op1, .operand2 = rfl::Tuple(_op2)}; + } } else { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } } diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp index 889e14e7..c0e168c2 100644 --- a/tests/postgres/test_select_from_with_timestamps.cpp +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -55,9 +55,10 @@ TEST(postgres, test_range_select_from_with_timestamps) { const auto get_birthdays = select_from( ("birthday"_c + std::chrono::days(10)) | as<"birthday">, - cast(concat(cast(year("birthday"_c)), "-", - cast(month("birthday"_c)), "-", - cast(day("birthday"_c)))) | + ((cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c))))) + + std::chrono::days(10)) | as<"birthday_recreated">, days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, unixepoch("birthday"_c + std::chrono::days(10)) | @@ -74,9 +75,10 @@ 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) 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-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}])"; + 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}])"; EXPECT_EQ(postgres::to_sql(get_birthdays), expected_query); EXPECT_EQ(rfl::json::write(birthdays), expected); diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp index 1fbc16b2..c1d92cb3 100644 --- a/tests/sqlite/test_select_from_with_timestamps.cpp +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -48,9 +48,9 @@ TEST(sqlite, test_range_select_from_with_timestamps) { const auto get_birthdays = select_from( ("birthday"_c + std::chrono::days(10)) | as<"birthday">, - cast(concat(cast(year("birthday"_c)), "-", - cast(month("birthday"_c)), "-", - cast(day("birthday"_c)))) | + ((cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c)))))) | as<"birthday_recreated">, days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, unixepoch("birthday"_c + std::chrono::days(10)) | From 6ff8fcbc1bb79d826b94e3588388a74158b19d58 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 2 Jul 2025 19:23:27 +0200 Subject: [PATCH 10/12] Made sure this works on nullable timestamps --- include/sqlgen/transpilation/make_field.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index f422270d..a7d5dbec 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -235,7 +235,8 @@ struct MakeField>; using Operands = rfl::Tuple; - static_assert(is_timestamp_v, "Must be a timestamp."); + static_assert(is_timestamp_v>, + "Must be a timestamp."); dynamic::SelectFrom::Field operator()(const auto& _o) const { return dynamic::SelectFrom::Field{ From 005c5d56462dbb216624e34e4ad6e4384fa5d961 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 2 Jul 2025 19:23:58 +0200 Subject: [PATCH 11/12] Added blankspace --- src/sqlgen/postgres/to_sql.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 7f7460ff..156634ee 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -509,7 +509,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - stream << "extract(EPOCH FROM" << operation_to_sql(*_s.op1) << ")"; + stream << "extract(EPOCH FROM " << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { stream << "upper(" << operation_to_sql(*_s.op1) << ")"; From db0585b1adfae1a8f19ba776211cb8ed1354a54a Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 2 Jul 2025 19:52:35 +0200 Subject: [PATCH 12/12] Added documentation --- docs/README.md | 9 +- docs/mathematical_operations.md | 72 +++++++++ docs/null_handling_operations.md | 81 ++++++++++ docs/other_operations.md | 249 ----------------------------- docs/string_operations.md | 65 ++++++++ docs/timestamp_operations.md | 127 +++++++++++++++ docs/type_conversion_operations.md | 25 +++ 7 files changed, 378 insertions(+), 250 deletions(-) create mode 100644 docs/mathematical_operations.md create mode 100644 docs/null_handling_operations.md delete mode 100644 docs/other_operations.md create mode 100644 docs/string_operations.md create mode 100644 docs/timestamp_operations.md create mode 100644 docs/type_conversion_operations.md diff --git a/docs/README.md b/docs/README.md index de41ca5c..3f99bab1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,7 +26,14 @@ 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::insert](insert.md) - How to insert data within transactions - [sqlgen::update](update.md) - How to update data in a table -- [Other Operations and Functions](other_operations.md) - How to use SQL functions like `coalesce`, `concat`, `abs`, `cast`, and more + +## Other Operations + +- [Mathematical Operations](mathematical_operations.md) - How to use mathematical functions in queries (e.g., abs, ceil, floor, exp, trigonometric functions, round). +- [String Operations](string_operations.md) - How to manipulate and transform strings in queries (e.g., length, lower, upper, trim, replace, concat). +- [Type Conversion Operations](type_conversion_operations.md) - How to convert between types safely in queries (e.g., cast int to double). +- [Null Handling Operations](null_handling_operations.md) - How to handle nullable values and propagate nullability correctly (e.g., with coalesce and nullability rules). +- [Timestamp and Date/Time Functions](timestamp_operations.md) - How to work with timestamps, dates, and times (e.g., extract parts, perform arithmetic, convert formats). ## Data Types and Validation diff --git a/docs/mathematical_operations.md b/docs/mathematical_operations.md new file mode 100644 index 00000000..d0352be7 --- /dev/null +++ b/docs/mathematical_operations.md @@ -0,0 +1,72 @@ +# Mathematical Operations + +The `sqlgen` library provides a set of mathematical functions for use in queries. These functions are type-safe and map to the appropriate SQL operations for the target database. + +## Motivating Example +Suppose you want to analyze the ages of children in a family, grouped by their last name, and you want to compute the average, minimum, and maximum age (with some mathematical operations applied): + +```cpp +struct Children { + std::string last_name; + double avg_age; + double max_age_plus_one; + double min_age_plus_one; +}; + +const auto get_children = + select_from( + "last_name"_c, + round(avg(cast("age"_c))).as<"avg_age">(), + max(cast("age"_c) + 1.0).as<"max_age_plus_one">(), + (min(cast("age"_c)) + 1.0).as<"min_age_plus_one">() + ) + | where("age"_c < 18) + | group_by("last_name"_c) + | to>; +``` + +This query groups people by last name, filters for those under 18, and computes the average age (rounded), the maximum age plus one, and the minimum age plus one. + +## Mathematical Functions + +### `abs` +Returns the absolute value of a numeric expression. + +```cpp +abs("age"_c * (-1)) | as<"abs_age"> +``` + +### `ceil` / `floor` +Rounds a numeric value up (`ceil`) or down (`floor`) to the nearest integer. + +```cpp +ceil("salary"_c) | as<"salary_ceiled"> +floor("salary"_c) | as<"salary_floored"> +``` + +### `exp`, `ln`, `log2`, `sqrt` +- `exp(x)`: Exponential function (e^x) +- `ln(x)`: Natural logarithm +- `log2(x)`: Base-2 logarithm +- `sqrt(x)`: Square root + +```cpp +round(exp(cast("age"_c)), 2) | as<"exp_age"> +round(sqrt(cast("age"_c)), 2) | as<"sqrt_age"> +``` + +### `sin`, `cos`, `tan` +Trigonometric functions. + +```cpp +sin("angle"_c) | as<"sin_angle"> +cos("angle"_c) | as<"cos_angle"> +tan("angle"_c) | as<"tan_angle"> +``` + +### `round` +Rounds a numeric value to a specified number of decimal places. + +```cpp +round("price"_c, 2) | as<"rounded_price"> +``` \ No newline at end of file diff --git a/docs/null_handling_operations.md b/docs/null_handling_operations.md new file mode 100644 index 00000000..1863c740 --- /dev/null +++ b/docs/null_handling_operations.md @@ -0,0 +1,81 @@ +# Null Handling Operations + +The `sqlgen` library provides functions for handling null values in a type-safe way. These functions allow you to work with nullable columns and propagate nullability correctly in your queries. + +## Null Handling + +### `coalesce` +Returns the first non-null value in the argument list. + +```cpp +coalesce("last_name"_c, "none") | as<"last_name_or_none"> +coalesce(upper("last_name"_c), "none") | as<"last_name_or_none"> +``` + +--- + +## Nullable Values + +When using these operations on nullable columns (e.g., `std::optional`), the result will also be nullable if any operand is nullable. For example, adding two `std::optional` columns will yield a `std::optional`. The `coalesce` function is especially useful for providing default values for nullable columns. + +--- + +## Nullability Propagation and `coalesce` Semantics + +### General Nullability Rules + +- **Unary operations** (e.g., `abs`, `upper`, `sqrt`): + - If the operand is nullable (`std::optional`), the result is also nullable. + - If the operand is not nullable, the result is not nullable. +- **Binary or ternary operations** (e.g., `+`, `concat`, `replace`, etc.): + - If *any* operand is nullable, the result is nullable (`std::optional`). + - If *all* operands are non-nullable, the result is non-nullable. +- **Type conversion (`cast`)**: + - If the source is nullable, the result is nullable of the target type. + - If the source is not nullable, the result is not nullable. +- **String operations** (e.g., `concat`, `replace`, `ltrim`, `rtrim`, `trim`): + - If any input is nullable, the result is nullable. + - All string operands must have the same underlying type (checked at compile time). + +### `coalesce` Nullability Semantics + +The `coalesce` function returns the first non-null value from its arguments. Its nullability is determined as follows: + +- If **all** arguments are nullable, the result is nullable (`std::optional`). +- If **any** argument is non-nullable, the result is non-nullable (`T`). +- All arguments must have the same underlying type (ignoring nullability), enforced at compile time. + +#### Examples + +```cpp +// All arguments nullable: result is nullable +coalesce(std::optional{}, std::optional{}) // -> std::optional + +// At least one argument non-nullable: result is non-nullable +coalesce(std::optional{}, 42) // -> int +coalesce(42, std::optional{}) // -> int + +// All arguments non-nullable: result is non-nullable +coalesce(1, 2) // -> int + +// Mixed string example +coalesce(std::optional{}, "default") // -> std::string + +// Compile-time error: mismatched types +// coalesce(std::optional{}, std::optional{}) // Error +``` + +#### Practical Usage + +```cpp +// Provide a default for a nullable column +coalesce("last_name"_c, "none") | as<"last_name_or_none"> // Result is std::string +coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> +``` + +### Advanced: How sqlgen Enforces Nullability + +The nullability rules are enforced at compile time using template metaprogramming (see `underlying_t.hpp`). This ensures that: +- You cannot accidentally assign a nullable result to a non-nullable field. +- All arguments to `coalesce` must have the same base type (e.g., all `int` or all `std::string`). +- The result type of any operation is always correct and safe to use in your result structs. \ No newline at end of file diff --git a/docs/other_operations.md b/docs/other_operations.md deleted file mode 100644 index 5b993cbf..00000000 --- a/docs/other_operations.md +++ /dev/null @@ -1,249 +0,0 @@ -# Other Operations and Functions - -The `sqlgen` library provides a rich set of SQL operations and functions that can be used in a type-safe and composable way within C++ queries. These operations cover mathematical, string, type conversion, and null-handling functions, and are designed to closely mirror SQL's expressive power. - -## Usage - -You can use these functions in your `select_from` queries, often in combination with column expressions, literals, and other operations. All functions are available in the `sqlgen` namespace. - ---- - -## Mathematical Functions - -### `abs` -Returns the absolute value of a numeric expression. - -```cpp -abs("age"_c * (-1)) | as<"abs_age"> -``` - -### `ceil` / `floor` -Rounds a numeric value up (`ceil`) or down (`floor`) to the nearest integer. - -```cpp -ceil("salary"_c) | as<"salary_ceiled"> -floor("salary"_c) | as<"salary_floored"> -``` - -### `exp`, `ln`, `log2`, `sqrt` -- `exp(x)`: Exponential function (e^x) -- `ln(x)`: Natural logarithm -- `log2(x)`: Base-2 logarithm -- `sqrt(x)`: Square root - -```cpp -round(exp(cast("age"_c)), 2) | as<"exp_age"> -round(sqrt(cast("age"_c)), 2) | as<"sqrt_age"> -``` - -### `sin`, `cos`, `tan` -Trigonometric functions. - -```cpp -sin("angle"_c) | as<"sin_angle"> -cos("angle"_c) | as<"cos_angle"> -tan("angle"_c) | as<"tan_angle"> -``` - -### `round` -Rounds a numeric value to a specified number of decimal places. - -```cpp -round("price"_c, 2) | as<"rounded_price"> -``` - ---- - -## String Functions - -### `length` -Returns the length of a string. - -```cpp -length(trim("first_name"_c)) | as<"length_first_name"> -``` - -### `lower` / `upper` -Converts a string to lowercase or uppercase. - -```cpp -lower("first_name"_c) | as<"first_name_lower"> -upper("first_name"_c) | as<"first_name_upper"> -``` - -### `ltrim`, `rtrim`, `trim` -Removes whitespace (or a specified character) from the left, right, or both sides of a string. - -```cpp -ltrim("first_name"_c) | as<"ltrimmed_name"> -rtrim("last_name"_c) | as<"rtrimmed_name"> -trim("nickname"_c) | as<"trimmed_nickname"> -// With custom characters: -ltrim("field"_c, "_ ") | as<"ltrimmed_field"> -``` - -### `replace` -Replaces all occurrences of a substring with another substring. - -```cpp -replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> -``` - -### `concat` -Concatenates multiple strings or expressions. - -```cpp -concat("first_name"_c, " ", "last_name"_c) | as<"full_name"> -concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name"> -``` - ---- - -## Type Conversion - -### `cast` -Casts a value to a different type (e.g., int to double). - -```cpp -cast("age"_c) | as<"age_as_double"> -``` - ---- - -## Null Handling - -### `coalesce` -Returns the first non-null value in the argument list. - -```cpp -coalesce("last_name"_c, "none") | as<"last_name_or_none"> -coalesce(upper("last_name"_c), "none") | as<"last_name_or_none"> -``` - ---- - -## Nullable Values - -When using these operations on nullable columns (e.g., `std::optional`), the result will also be nullable if any operand is nullable. For example, adding two `std::optional` columns will yield a `std::optional`. The `coalesce` function is especially useful for providing default values for nullable columns. - ---- - -## Nullability Propagation and `coalesce` Semantics - -### General Nullability Rules - -- **Unary operations** (e.g., `abs`, `upper`, `sqrt`): - - If the operand is nullable (`std::optional`), the result is also nullable. - - If the operand is not nullable, the result is not nullable. -- **Binary or ternary operations** (e.g., `+`, `concat`, `replace`, etc.): - - If *any* operand is nullable, the result is nullable (`std::optional`). - - If *all* operands are non-nullable, the result is non-nullable. -- **Type conversion (`cast`)**: - - If the source is nullable, the result is nullable of the target type. - - If the source is not nullable, the result is not nullable. -- **String operations** (e.g., `concat`, `replace`, `ltrim`, `rtrim`, `trim`): - - If any input is nullable, the result is nullable. - - All string operands must have the same underlying type (checked at compile time). - -### `coalesce` Nullability Semantics - -The `coalesce` function returns the first non-null value from its arguments. Its nullability is determined as follows: - -- If **all** arguments are nullable, the result is nullable (`std::optional`). -- If **any** argument is non-nullable, the result is non-nullable (`T`). -- All arguments must have the same underlying type (ignoring nullability), enforced at compile time. - -#### Examples - -```cpp -// All arguments nullable: result is nullable -coalesce(std::optional{}, std::optional{}) // -> std::optional - -// At least one argument non-nullable: result is non-nullable -coalesce(std::optional{}, 42) // -> int -coalesce(42, std::optional{}) // -> int - -// All arguments non-nullable: result is non-nullable -coalesce(1, 2) // -> int - -// Mixed string example -coalesce(std::optional{}, "default") // -> std::string - -// Compile-time error: mismatched types -// coalesce(std::optional{}, std::optional{}) // Error -``` - -#### Practical Usage - -```cpp -// Provide a default for a nullable column -coalesce("last_name"_c, "none") | as<"last_name_or_none"> // Result is std::string -coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> -``` - -### Advanced: How sqlgen Enforces Nullability - -The nullability rules are enforced at compile time using template metaprogramming (see `underlying_t.hpp`). This ensures that: -- You cannot accidentally assign a nullable result to a non-nullable field. -- All arguments to `coalesce` must have the same base type (e.g., all `int` or all `std::string`). -- The result type of any operation is always correct and safe to use in your result structs. - ---- - -## Example: Combining Operations - -```cpp -struct Children { - int id_plus_age; - int age_times_2; - int id_plus_2_minus_age; - int abs_age; - double exp_age; - double sqrt_age; - size_t length_first_name; - std::string full_name; - std::string first_name_lower; - std::string first_name_upper; - std::string first_name_replaced; -}; - -const auto get_children = select_from( - ("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - abs("age"_c * (-1)) | as<"abs_age">, - round(exp(cast("age"_c)), 2) | as<"exp_age">, - round(sqrt(cast("age"_c)), 2) | as<"sqrt_age">, - length(trim("first_name"_c)) | as<"length_first_name">, - concat("first_name"_c, " ", "last_name"_c) | as<"full_name">, - lower("first_name"_c) | as<"first_name_lower">, - upper("first_name"_c, " ") | as<"first_name_upper">, - replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> -) | where("age"_c < 18) | to>; -``` - -This generates the following SQL: - -```sql -SELECT - ("id" + "age") AS "id_plus_age", - ("age" * 2) AS "age_times_2", - ABS(("age" * -1)) AS "abs_age", - ROUND(EXP(CAST("age" AS NUMERIC)), 2) AS "exp_age", - ROUND(SQRT(CAST("age" AS NUMERIC)), 2) AS "sqrt_age", - LENGTH(TRIM("first_name")) AS "length_first_name", - ("first_name" || ' ' || "last_name") AS "full_name", - LOWER("first_name") AS "first_name_lower", - UPPER("first_name") AS "first_name_upper", - REPLACE("first_name", 'Bart', 'Hugo') AS "first_name_replaced" -FROM "Person" -WHERE "age" < 18; -``` - ---- - -## Notes - -- All functions are type-safe and map to the appropriate SQL operations for the target database. -- You can chain and nest operations as needed. -- Use the `as<"alias">(...)` or `| as<"alias">` syntax to alias expressions for mapping to struct fields. - diff --git a/docs/string_operations.md b/docs/string_operations.md new file mode 100644 index 00000000..54c5f556 --- /dev/null +++ b/docs/string_operations.md @@ -0,0 +1,65 @@ +# String Operations + +The `sqlgen` library provides a set of string functions for use in queries. These functions are type-safe and map to the appropriate SQL operations for the target database. + +## Motivating Example +Suppose you want to create a report of people with their full names in uppercase, and also want to trim any whitespace from their last names: + +```cpp +struct PersonReport { + std::string full_name; + std::string last_name_trimmed; +}; + +const auto get_reports = + select_from( + concat(upper("last_name"_c), ", ", "first_name"_c).as<"full_name">(), + trim("last_name"_c).as<"last_name_trimmed">() + ) + | to>; +``` + +This query produces a list of people with their full names in the format "LASTNAME, Firstname" (with the last name in uppercase) and ensures the last name has no leading or trailing whitespace. + +## String Functions + +### `length` +Returns the length of a string. + +```cpp +length(trim("first_name"_c)) | as<"length_first_name"> +``` + +### `lower` / `upper` +Converts a string to lowercase or uppercase. + +```cpp +lower("first_name"_c) | as<"first_name_lower"> +upper("first_name"_c) | as<"first_name_upper"> +``` + +### `ltrim`, `rtrim`, `trim` +Removes whitespace (or a specified character) from the left, right, or both sides of a string. + +```cpp +ltrim("first_name"_c) | as<"ltrimmed_name"> +rtrim("last_name"_c) | as<"rtrimmed_name"> +trim("nickname"_c) | as<"trimmed_nickname"> +// With custom characters: +ltrim("field"_c, "_ ") | as<"ltrimmed_field"> +``` + +### `replace` +Replaces all occurrences of a substring with another substring. + +```cpp +replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> +``` + +### `concat` +Concatenates multiple strings or expressions. + +```cpp +concat("first_name"_c, " ", "last_name"_c) | as<"full_name"> +concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name"> +``` \ No newline at end of file diff --git a/docs/timestamp_operations.md b/docs/timestamp_operations.md new file mode 100644 index 00000000..f4b19a77 --- /dev/null +++ b/docs/timestamp_operations.md @@ -0,0 +1,127 @@ +# Timestamp and Date/Time Functions + +The `sqlgen` library provides a comprehensive set of operations for working with timestamps, dates, and times. These functions allow you to extract date/time parts, perform arithmetic, and convert between types in a type-safe and composable way. All timestamp operations work with the `sqlgen::Timestamp<...>` type (including the provided aliases `Date` and `DateTime`). + +### Timestamp Types + +- `Timestamp`: Represents a timestamp with a custom format (see `sqlgen::Timestamp`). +- `Date`: Alias for `Timestamp<"%Y-%m-%d">` (date only). +- `DateTime`: Alias for `Timestamp<"%Y-%m-%d %H:%M:%S">` (date and time). + +--- + +### Date/Time Extraction Functions + +These functions extract parts of a timestamp or date: + +- `year(ts)`: Extracts the year as an integer. +- `month(ts)`: Extracts the month (1-12). +- `day(ts)`: Extracts the day of the month (1-31). +- `hour(ts)`: Extracts the hour (0-23). +- `minute(ts)`: Extracts the minute (0-59). +- `second(ts)`: Extracts the second (0-59). +- `weekday(ts)`: Extracts the day of the week (0 = Sunday for SQLite, 0 = Monday for Postgres). + +```cpp +year("birthday"_c) | as<"year"> +month("birthday"_c) | as<"month"> +day("birthday"_c) | as<"day"> +hour("birthday"_c) | as<"hour"> +minute("birthday"_c) | as<"minute"> +second("birthday"_c) | as<"second"> +weekday("birthday"_c) | as<"weekday"> +``` + +--- + +### Timestamp Arithmetic + +You can add or subtract durations (e.g., days, years, weeks, milliseconds) to/from timestamp columns using standard C++ chrono types: + +```cpp +("birthday"_c + std::chrono::days(10)) | as<"birthday_plus_10"> +("birthday"_c + std::chrono::years(1) - std::chrono::weeks(2)) | as<"birthday_shifted"> +``` + +This is translated to the appropriate SQL for the backend (e.g., `datetime(..., '+10 days')` for SQLite, `+ INTERVAL '10 days'` for Postgres). + +--- + +### Days Between + +Calculates the number of days between two timestamps or dates: + +```cpp +days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days"> +``` + +--- + +### Unix Epoch Conversion + +Converts a timestamp to the number of seconds since the Unix epoch: + +```cpp +unixepoch("birthday"_c) | as<"birthday_unixepoch"> +unixepoch("birthday"_c + std::chrono::days(10)) | as<"birthday_unixepoch_plus_10"> +``` + +--- + +### Timestamp Construction and Conversion + +You can construct or cast timestamps from strings or from extracted parts: + +```cpp +cast(concat(cast(year("birthday"_c)), "-", cast(month("birthday"_c)), "-", cast(day("birthday"_c)))) | as<"birthday_recreated"> +``` + +--- + +### Example: Working with Timestamps + +```cpp +struct Birthday { + Date birthday; + Date birthday_recreated; + time_t birthday_unixepoch; + double age_in_days; + int hour; + int minute; + int second; + int weekday; +}; + +const auto get_birthdays = select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + (cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c))))) | as<"birthday_recreated">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, + minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday"> +) | order_by("id"_c) | to>; +``` + +This generates SQL like: + +**SQLite:** +```sql +SELECT datetime("birthday", '+10 days') AS "birthday", ... +``` +**Postgres:** +```sql +SELECT "birthday" + INTERVAL '10 days' AS "birthday", ... +``` + +--- + +### Notes + +- All timestamp operations are type-safe and propagate nullability as described above. +- You can chain and nest timestamp operations with other operations. +- Duration arithmetic supports `std::chrono::days`, `std::chrono::years`, `std::chrono::weeks`, `std::chrono::milliseconds`, etc. +- The result types and SQL translation may differ slightly between SQLite and Postgres, but the C++ interface is unified. \ No newline at end of file diff --git a/docs/type_conversion_operations.md b/docs/type_conversion_operations.md new file mode 100644 index 00000000..0ea486d5 --- /dev/null +++ b/docs/type_conversion_operations.md @@ -0,0 +1,25 @@ +# Type Conversion + +Casts a value to a different type (e.g., int to double). + +## Motivating Example +Suppose you want to store birthdays as strings in your database, but you need to convert them to date types for calculations, or you want to cast an integer column to double for mathematical operations: + +```cpp +struct BirthdayInfo { + Date birthday; + double age_in_days; +}; + +const auto get_birthdays = + select_from( + cast("birthday"_c).as<"birthday">(), + days_between("birthday"_c, Date("2011-01-01")).as<"age_in_days">() + ) + | to>; +``` + +This query casts the `birthday` column to a `Date` type and calculates the number of days between each birthday and a reference date. + +```cpp +cast("age"_c) | as<"age_as_double">