From 5bec407c18093fa1a2b019698bc3a48d4383c3f4 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 13 Jun 2025 23:26:45 +0200 Subject: [PATCH 01/25] Started developing support for operators --- include/sqlgen/dynamic/Operation.hpp | 50 ++++++++++++++++++++++++++++ src/sqlgen/postgres/to_sql.cpp | 38 +++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 include/sqlgen/dynamic/Operation.hpp diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp new file mode 100644 index 00000000..728019d4 --- /dev/null +++ b/include/sqlgen/dynamic/Operation.hpp @@ -0,0 +1,50 @@ +#ifndef SQLGEN_DYNAMIC_OPERATION_HPP_ +#define SQLGEN_DYNAMIC_OPERATION_HPP_ + +#include + +#include "../Ref.hpp" +#include "Aggregation.hpp" +#include "Column.hpp" +#include "ColumnOrValue.hpp" +#include "Value.hpp" + +namespace sqlgen::dynamic { + +struct Operation { + struct Constant { + Value val; + }; + + struct Divides { + Ref op1; + Ref op2; + }; + + struct Minus { + Ref op1; + Ref op2; + }; + + struct Multiplies { + Ref op1; + Ref op2; + }; + + struct Plus { + Ref op1; + Ref op2; + }; + + using ReflectionType = + Ref>; + + const ReflectionType& reflection() const { return val; } + + ReflectionType val; +}; + +} // namespace sqlgen::dynamic + +#endif diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 80798f66..8c1b6edd 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -1,11 +1,13 @@ #include "sqlgen/postgres/to_sql.hpp" +#include #include #include #include #include #include +#include "sqlgen/dynamic/Operation.hpp" #include "sqlgen/internal/collect/vector.hpp" #include "sqlgen/internal/strings/strings.hpp" @@ -43,6 +45,8 @@ std::vector get_primary_keys( std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept; +std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept; + std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; std::string type_to_sql(const dynamic::Type& _type) noexcept; @@ -369,6 +373,40 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { return stream.str(); } +std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { + return _stmt.val->visit([](const auto& _s) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return aggregation_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return column_or_value_to_sql(_s.val); + + } else if constexpr (std::is_same_v) { + return column_or_value_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return std::format("({}) / ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) - ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) * ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) + ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + }); +} + std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept { using namespace std::ranges::views; From 3833edb7d73140fd76aaf0b775b8e655ebe655d3 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sat, 14 Jun 2025 23:16:15 +0200 Subject: [PATCH 02/25] Added operations to select_from --- include/sqlgen/dynamic/Operation.hpp | 9 +-- include/sqlgen/dynamic/SelectFrom.hpp | 6 +- include/sqlgen/dynamic/Value.hpp | 6 +- include/sqlgen/transpilation/make_field.hpp | 56 ++++++++++++----- include/sqlgen/transpilation/operations.hpp | 46 ++++++++++++++ include/sqlgen/transpilation/to_value.hpp | 6 +- src/sqlgen/postgres/to_sql.cpp | 40 +++++------- src/sqlgen/sqlite/to_sql.cpp | 70 ++++++++++++++------- 8 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 include/sqlgen/transpilation/operations.hpp diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 728019d4..a473ba46 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -12,10 +12,6 @@ namespace sqlgen::dynamic { struct Operation { - struct Constant { - Value val; - }; - struct Divides { Ref op1; Ref op2; @@ -36,9 +32,8 @@ struct Operation { Ref op2; }; - using ReflectionType = - Ref>; + using ReflectionType = rfl::TaggedUnion<"what", Aggregation, Column, Divides, + Minus, Multiplies, Plus, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/dynamic/SelectFrom.hpp b/include/sqlgen/dynamic/SelectFrom.hpp index b9d25c39..6876a96b 100644 --- a/include/sqlgen/dynamic/SelectFrom.hpp +++ b/include/sqlgen/dynamic/SelectFrom.hpp @@ -6,20 +6,18 @@ #include #include -#include "Aggregation.hpp" -#include "Column.hpp" #include "Condition.hpp" #include "GroupBy.hpp" #include "Limit.hpp" +#include "Operation.hpp" #include "OrderBy.hpp" #include "Table.hpp" -#include "Value.hpp" namespace sqlgen::dynamic { struct SelectFrom { struct Field { - rfl::TaggedUnion<"type", Aggregation, Column, Value> val; + Operation val; std::optional as; }; diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index 4e1d9343..c6b32594 100644 --- a/include/sqlgen/dynamic/Value.hpp +++ b/include/sqlgen/dynamic/Value.hpp @@ -18,7 +18,11 @@ struct String { std::string val; }; -using Value = rfl::TaggedUnion<"type", Float, Integer, String>; +struct Value { + using ReflectionType = rfl::TaggedUnion<"type", Float, Integer, String>; + const auto& reflection() const { return val; } + ReflectionType val; +}; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 5326cc3e..4e3922cc 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -12,6 +12,7 @@ #include "Value.hpp" #include "aggregations.hpp" #include "all_columns_exist.hpp" +#include "operations.hpp" #include "remove_nullable_t.hpp" #include "to_value.hpp" #include "underlying_t.hpp" @@ -30,7 +31,8 @@ struct MakeField { using Type = ValueType; dynamic::SelectFrom::Field operator()(const auto& _val) const { - return dynamic::SelectFrom::Field{.val = to_value(_val)}; + return dynamic::SelectFrom::Field{ + dynamic::Operation{.val = to_value(_val)}}; } }; @@ -46,8 +48,8 @@ struct MakeField> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{.val = - dynamic::Column{.name = _name.str()}}; + return dynamic::SelectFrom::Field{ + dynamic::Operation{.val = dynamic::Column{.name = _name.str()}}}; } }; @@ -64,8 +66,11 @@ struct MakeField> { dynamic::SelectFrom::Field operator()(const auto& _as) const { return dynamic::SelectFrom::Field{ - .val = MakeField>{}(_as.val) - .val, + .val = + dynamic::Operation{ + .val = MakeField>{}( + _as.val) + .val.val}, .as = _new_name.str()}; } }; @@ -89,9 +94,9 @@ struct MakeField>> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Avg{ - .val = dynamic::Column{.name = _name.str()}}}}; + .val = dynamic::Column{.name = _name.str()}}}}}; } }; @@ -108,11 +113,11 @@ struct MakeField>> { using Type = size_t; dynamic::SelectFrom::Field operator()(const auto& _agg) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Count{ .val = dynamic::Column{.name = _name.str()}, .distinct = _agg.distinct}}, - }; + }}; } }; @@ -125,10 +130,10 @@ struct MakeField> { using Type = size_t; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{ dynamic::Aggregation::Count{.val = std::nullopt, .distinct = false}, - }}; + }}}; } }; @@ -144,9 +149,9 @@ struct MakeField>> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Max{ - .val = dynamic::Column{.name = _name.str()}}}}; + .val = dynamic::Column{.name = _name.str()}}}}}; } }; @@ -162,9 +167,9 @@ struct MakeField>> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Min{ - .val = dynamic::Column{.name = _name.str()}}}}; + .val = dynamic::Column{.name = _name.str()}}}}}; } }; @@ -187,9 +192,26 @@ struct MakeField>> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{ + return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Sum{ - .val = dynamic::Column{.name = _name.str()}}}}; + .val = dynamic::Column{.name = _name.str()}}}}}; + } +}; + +template +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::Divides{ + .op1 = MakeField>{}(_o.op1) + .val, + .op2 = MakeField>{}(_o.op2) + .val}}}; } }; diff --git a/include/sqlgen/transpilation/operations.hpp b/include/sqlgen/transpilation/operations.hpp new file mode 100644 index 00000000..45397a62 --- /dev/null +++ b/include/sqlgen/transpilation/operations.hpp @@ -0,0 +1,46 @@ +#ifndef SQLGEN_TRANSPILATION_OPERATIONS_HPP_ +#define SQLGEN_TRANSPILATION_OPERATIONS_HPP_ + +#include + +namespace sqlgen::transpilation::operations { + +template +struct Divides { + using Op1Type = _Op1Type; + using Op2Type = _Op2Type; + + Op1Type op1; + Op2Type op2; +}; + +template +struct Minus { + using Op1Type = _Op1Type; + using Op2Type = _Op2Type; + + Op1Type op1; + Op2Type op2; +}; + +template +struct Multiplies { + using Op1Type = _Op1Type; + using Op2Type = _Op2Type; + + Op1Type op1; + Op2Type op2; +}; + +template +struct Plus { + using Op1Type = _Op1Type; + using Op2Type = _Op2Type; + + Op1Type op1; + Op2Type op2; +}; + +} // namespace sqlgen::transpilation::operations + +#endif diff --git a/include/sqlgen/transpilation/to_value.hpp b/include/sqlgen/transpilation/to_value.hpp index 4f0f36e7..b0f9dd4e 100644 --- a/include/sqlgen/transpilation/to_value.hpp +++ b/include/sqlgen/transpilation/to_value.hpp @@ -14,13 +14,13 @@ template dynamic::Value to_value(const T& _t) { using Type = std::remove_cvref_t; if constexpr (std::is_floating_point_v) { - return dynamic::Float{.val = static_cast(_t)}; + return dynamic::Value{dynamic::Float{.val = static_cast(_t)}}; } else if constexpr (std::is_integral_v) { - return dynamic::Integer{.val = static_cast(_t)}; + return dynamic::Value{dynamic::Integer{.val = static_cast(_t)}}; } else if constexpr (std::is_convertible_v) { - return dynamic::String{.val = std::string(_t)}; + return dynamic::Value{dynamic::String{.val = std::string(_t)}}; } else if constexpr (has_reflection_method) { return to_value(_t.reflection()); diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 8c1b6edd..7fe1bb94 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -73,34 +73,29 @@ std::string add_not_null_if_necessary( std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { - std::stringstream stream; - using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("AVG({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "COUNT(" - << std::string(_agg.val && _agg.distinct ? " DISTINCT " : "") - << (_agg.val ? column_or_value_to_sql(*_agg.val) - : std::string("*")) - << ")"; + const auto val = + std::string(_agg.val && _agg.distinct ? "DISTINCT " : "") + + (_agg.val ? column_or_value_to_sql(*_agg.val) : std::string("*")); + return std::format("COUNT({})", val); } else if constexpr (std::is_same_v) { - stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("MAX({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("MIN({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("SUM({})", column_or_value_to_sql(_agg.val)); } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } - - return stream.str(); }); } @@ -120,7 +115,7 @@ std::string column_or_value_to_sql( if constexpr (std::is_same_v) { return wrap_in_quotes(_c.name); } else { - return _c.visit(handle_value); + return _c.val.visit(handle_value); } }); } @@ -313,14 +308,7 @@ std::string escape_single_quote(const std::string& _str) noexcept { std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept { std::stringstream stream; - stream << _field.val.visit([](const auto& _val) -> std::string { - using Type = std::remove_cvref_t; - if constexpr (std::is_same_v) { - return aggregation_to_sql(_val); - } else { - return column_or_value_to_sql(_val); - } - }); + stream << operation_to_sql(_field.val); if (_field.as) { stream << " AS " << wrap_in_quotes(*_field.as); @@ -374,14 +362,11 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { } std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { - return _stmt.val->visit([](const auto& _s) -> std::string { + return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { return aggregation_to_sql(_s); - } else if constexpr (std::is_same_v) { - return column_or_value_to_sql(_s.val); - } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); @@ -401,6 +386,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return column_or_value_to_sql(_s); + } 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 9c187639..64f47f94 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -1,7 +1,9 @@ +#include #include #include #include +#include "sqlgen/dynamic/Operation.hpp" #include "sqlgen/internal/collect/vector.hpp" #include "sqlgen/internal/strings/strings.hpp" #include "sqlgen/sqlite/Connection.hpp" @@ -36,6 +38,8 @@ std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept; template std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept; +std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept; + std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept; std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept; @@ -47,34 +51,29 @@ std::string update_to_sql(const dynamic::Update& _stmt) noexcept; std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { - std::stringstream stream; - using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("AVG({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "COUNT(" - << std::string(_agg.val && _agg.distinct ? " DISTINCT " : "") - << (_agg.val ? column_or_value_to_sql(*_agg.val) - : std::string("*")) - << ")"; + const auto val = + std::string(_agg.val && _agg.distinct ? "DISTINCT " : "") + + (_agg.val ? column_or_value_to_sql(*_agg.val) : std::string("*")); + return std::format("COUNT({})", val); } else if constexpr (std::is_same_v) { - stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("MAX({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("MIN({})", column_or_value_to_sql(_agg.val)); } else if constexpr (std::is_same_v) { - stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; + return std::format("SUM({})", column_or_value_to_sql(_agg.val)); } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } - - return stream.str(); }); } @@ -94,7 +93,7 @@ std::string column_or_value_to_sql( if constexpr (std::is_same_v) { return "\"" + _c.name + "\""; } else { - return _c.visit(handle_value); + return _c.val.visit(handle_value); } }); } @@ -280,14 +279,7 @@ std::string escape_single_quote(const std::string& _str) noexcept { std::string field_to_str(const dynamic::SelectFrom::Field& _field) noexcept { std::stringstream stream; - stream << _field.val.visit([](const auto& _val) -> std::string { - using Type = std::remove_cvref_t; - if constexpr (std::is_same_v) { - return aggregation_to_sql(_val); - } else { - return column_or_value_to_sql(_val); - } - }); + stream << operation_to_sql(_field.val); if (_field.as) { stream << " AS " << "\"" << *_field.as << "\""; @@ -329,6 +321,40 @@ std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept { return stream.str(); } +std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { + return _stmt.val.visit([](const auto& _s) -> std::string { + using Type = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return aggregation_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return column_or_value_to_sql(_s); + + } else if constexpr (std::is_same_v) { + return std::format("({}) / ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) - ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) * ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return std::format("({}) + ({})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + + } else if constexpr (std::is_same_v) { + return column_or_value_to_sql(_s); + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + }); +} + std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept { return std::string(_p.primary ? " PRIMARY KEY" : "") + std::string(_p.nullable ? "" : " NOT NULL"); From 29c81e691427c772ff1a164d58b6c7ce8526590a Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 15 Jun 2025 11:53:34 +0200 Subject: [PATCH 03/25] Redesigned the operations --- include/sqlgen/transpilation/Operation.hpp | 25 ++++++++++ include/sqlgen/transpilation/Operator.hpp | 10 ++++ .../transpilation/dynamic_operator_t.hpp | 44 ++++++++++++++++++ include/sqlgen/transpilation/make_field.hpp | 25 ++++++---- include/sqlgen/transpilation/operations.hpp | 46 ------------------- 5 files changed, 95 insertions(+), 55 deletions(-) create mode 100644 include/sqlgen/transpilation/Operation.hpp create mode 100644 include/sqlgen/transpilation/Operator.hpp create mode 100644 include/sqlgen/transpilation/dynamic_operator_t.hpp delete mode 100644 include/sqlgen/transpilation/operations.hpp diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp new file mode 100644 index 00000000..fc574859 --- /dev/null +++ b/include/sqlgen/transpilation/Operation.hpp @@ -0,0 +1,25 @@ +#ifndef SQLGEN_TRANSPILATION_OPERATION_HPP_ +#define SQLGEN_TRANSPILATION_OPERATION_HPP_ + +#include +#include + +#include "../Result.hpp" +#include "Operator.hpp" + +namespace sqlgen::transpilation { + +template +struct Operation { + static constexpr Operator op = _op; + + using Operand1Type = _Operand1Type; + using Operand2Type = _Operand2Type; + + Operand1Type operand1; + Operand2Type operand2; +}; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp new file mode 100644 index 00000000..805b992e --- /dev/null +++ b/include/sqlgen/transpilation/Operator.hpp @@ -0,0 +1,10 @@ +#ifndef SQLGEN_TRANSPILATION_OPERATOR_HPP_ +#define SQLGEN_TRANSPILATION_OPERATOR_HPP_ + +namespace sqlgen::transpilation { + +enum class Operator { divides, minus, multiplies, plus }; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp new file mode 100644 index 00000000..d36be821 --- /dev/null +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -0,0 +1,44 @@ +#ifndef SQLGEN_TRANSPILATION_DYNAMICOPERATORT_HPP_ +#define SQLGEN_TRANSPILATION_DYNAMICOPERATORT_HPP_ + +#include "../dynamic/Operation.hpp" +#include "Operator.hpp" + +namespace sqlgen::transpilation { + +template +struct DynamicOperator; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + using Type = dynamic::Operation::Divides; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + using Type = dynamic::Operation::Minus; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + using Type = dynamic::Operation::Multiplies; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + using Type = dynamic::Operation::Plus; +}; + +template +using dynamic_operator_t = typename DynamicOperator::Type; + +template +inline constexpr size_t num_operands_v = DynamicOperator::num_operands; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 4e3922cc..8d8f025b 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -9,10 +9,11 @@ #include "../dynamic/SelectFrom.hpp" #include "As.hpp" #include "Col.hpp" +#include "Operation.hpp" #include "Value.hpp" #include "aggregations.hpp" #include "all_columns_exist.hpp" -#include "operations.hpp" +#include "dynamic_operator_t.hpp" #include "remove_nullable_t.hpp" #include "to_value.hpp" #include "underlying_t.hpp" @@ -198,20 +199,26 @@ struct MakeField>> { } }; -template -struct MakeField> { +template +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; using Name = Nothing; dynamic::SelectFrom::Field operator()(const auto& _o) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{dynamic::Operation::Divides{ - .op1 = MakeField>{}(_o.op1) - .val, - .op2 = MakeField>{}(_o.op2) - .val}}}; + using DynamicOperatorType = dynamic_operator_t<_op>; + constexpr auto num_operands = num_operands_v<_op>; + if constexpr (num_operands == 2) { + return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ + .op1 = MakeField>{}( + _o.operand1) + .val, + .op2 = MakeField>{}( + _o.operand2) + .val}}}; + } } }; diff --git a/include/sqlgen/transpilation/operations.hpp b/include/sqlgen/transpilation/operations.hpp deleted file mode 100644 index 45397a62..00000000 --- a/include/sqlgen/transpilation/operations.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef SQLGEN_TRANSPILATION_OPERATIONS_HPP_ -#define SQLGEN_TRANSPILATION_OPERATIONS_HPP_ - -#include - -namespace sqlgen::transpilation::operations { - -template -struct Divides { - using Op1Type = _Op1Type; - using Op2Type = _Op2Type; - - Op1Type op1; - Op2Type op2; -}; - -template -struct Minus { - using Op1Type = _Op1Type; - using Op2Type = _Op2Type; - - Op1Type op1; - Op2Type op2; -}; - -template -struct Multiplies { - using Op1Type = _Op1Type; - using Op2Type = _Op2Type; - - Op1Type op1; - Op2Type op2; -}; - -template -struct Plus { - using Op1Type = _Op1Type; - using Op2Type = _Op2Type; - - Op1Type op1; - Op2Type op2; -}; - -} // namespace sqlgen::transpilation::operations - -#endif From 82d695cadaf1c4fc6d2bdaa398f143f3eec5b2e9 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 15 Jun 2025 12:46:50 +0200 Subject: [PATCH 04/25] Redesigned aggregations --- include/sqlgen/aggregations.hpp | 24 ++++-- include/sqlgen/transpilation/Aggregation.hpp | 31 +++++++ .../sqlgen/transpilation/AggregationOp.hpp | 10 +++ include/sqlgen/transpilation/aggregations.hpp | 81 ----------------- .../transpilation/dynamic_aggregation_t.hpp | 42 +++++++++ include/sqlgen/transpilation/make_field.hpp | 86 ++++--------------- 6 files changed, 114 insertions(+), 160 deletions(-) create mode 100644 include/sqlgen/transpilation/Aggregation.hpp create mode 100644 include/sqlgen/transpilation/AggregationOp.hpp delete mode 100644 include/sqlgen/transpilation/aggregations.hpp create mode 100644 include/sqlgen/transpilation/dynamic_aggregation_t.hpp diff --git a/include/sqlgen/aggregations.hpp b/include/sqlgen/aggregations.hpp index 34279034..17e50c83 100644 --- a/include/sqlgen/aggregations.hpp +++ b/include/sqlgen/aggregations.hpp @@ -5,47 +5,55 @@ #include #include "col.hpp" -#include "transpilation/aggregations.hpp" +#include "transpilation/Aggregation.hpp" +#include "transpilation/AggregationOp.hpp" namespace sqlgen { template auto avg(const Col<_name>&) { - return transpilation::aggregations::Avg>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}}; } inline auto count() { - return transpilation::aggregations::Count{}; + return transpilation::Aggregation{}; } template auto count(const Col<_name>&) { - return transpilation::aggregations::Count>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}}; } template auto count_distinct(const Col<_name>&) { - return transpilation::aggregations::Count>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}, .distinct = true}; } template auto max(const Col<_name>&) { - return transpilation::aggregations::Max>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}}; } template auto min(const Col<_name>&) { - return transpilation::aggregations::Min>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}}; } template auto sum(const Col<_name>&) { - return transpilation::aggregations::Sum>{ + return transpilation::Aggregation>{ .val = transpilation::Col<_name>{}}; } diff --git a/include/sqlgen/transpilation/Aggregation.hpp b/include/sqlgen/transpilation/Aggregation.hpp new file mode 100644 index 00000000..8e8033ea --- /dev/null +++ b/include/sqlgen/transpilation/Aggregation.hpp @@ -0,0 +1,31 @@ +#ifndef SQLGEN_TRANSPILATION_AGGREGATION_HPP_ +#define SQLGEN_TRANSPILATION_AGGREGATION_HPP_ + +#include + +#include "AggregationOp.hpp" +#include "As.hpp" + +namespace sqlgen::transpilation { + +/// To be used when we want to count everything. +struct All {}; + +template +struct Aggregation { + static constexpr auto agg = _agg; + using ValueType = _ValueType; + + template + auto as() const noexcept { + using T = std::remove_cvref_t; + return transpilation::As{.val = *this}; + } + + ValueType val; + bool distinct = false; +}; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/AggregationOp.hpp b/include/sqlgen/transpilation/AggregationOp.hpp new file mode 100644 index 00000000..f4edb243 --- /dev/null +++ b/include/sqlgen/transpilation/AggregationOp.hpp @@ -0,0 +1,10 @@ +#ifndef SQLGEN_TRANSPILATION_AGGREGATIONOP_HPP_ +#define SQLGEN_TRANSPILATION_AGGREGATIONOP_HPP_ + +namespace sqlgen::transpilation { + +enum class AggregationOp { avg, count, max, min, sum }; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/aggregations.hpp b/include/sqlgen/transpilation/aggregations.hpp deleted file mode 100644 index c40e1802..00000000 --- a/include/sqlgen/transpilation/aggregations.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef SQLGEN_TRANSPILATION_AGGREGATIONS_HPP_ -#define SQLGEN_TRANSPILATION_AGGREGATIONS_HPP_ - -#include - -#include "As.hpp" - -namespace sqlgen::transpilation::aggregations { - -/// To be used when we want to count everything. -struct All {}; - -template -struct Avg { - using ValueType = _ValueType; - - template - auto as() const noexcept { - using T = std::remove_cvref_t; - return transpilation::As{.val = *this}; - } - - ValueType val; -}; - -template -struct Count { - using ValueType = _ValueType; - - template - auto as() const noexcept { - using T = std::remove_cvref_t; - return transpilation::As{.val = *this}; - } - - ValueType val; - bool distinct = false; -}; - -template -struct Max { - using ValueType = _ValueType; - - template - auto as() const noexcept { - using T = std::remove_cvref_t; - return transpilation::As{.val = *this}; - } - - ValueType val; -}; - -template -struct Min { - using ValueType = _ValueType; - - template - auto as() const noexcept { - using T = std::remove_cvref_t; - return transpilation::As{.val = *this}; - } - - ValueType val; -}; - -template -struct Sum { - using ValueType = _ValueType; - - template - auto as() const noexcept { - using T = std::remove_cvref_t; - return transpilation::As{.val = *this}; - } - - ValueType val; -}; - -} // namespace sqlgen::transpilation::aggregations - -#endif diff --git a/include/sqlgen/transpilation/dynamic_aggregation_t.hpp b/include/sqlgen/transpilation/dynamic_aggregation_t.hpp new file mode 100644 index 00000000..bc8177fe --- /dev/null +++ b/include/sqlgen/transpilation/dynamic_aggregation_t.hpp @@ -0,0 +1,42 @@ +#ifndef SQLGEN_TRANSPILATION_DYNAMICAGGREGATIONT_HPP_ +#define SQLGEN_TRANSPILATION_DYNAMICAGGREGATIONT_HPP_ + +#include "../dynamic/Aggregation.hpp" +#include "AggregationOp.hpp" + +namespace sqlgen::transpilation { + +template +struct DynamicAggregation; + +template <> +struct DynamicAggregation { + using Type = dynamic::Aggregation::Avg; +}; + +template <> +struct DynamicAggregation { + using Type = dynamic::Aggregation::Count; +}; + +template <> +struct DynamicAggregation { + using Type = dynamic::Aggregation::Max; +}; + +template <> +struct DynamicAggregation { + using Type = dynamic::Aggregation::Min; +}; + +template <> +struct DynamicAggregation { + using Type = dynamic::Aggregation::Sum; +}; + +template +using dynamic_aggregation_t = typename DynamicAggregation::Type; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 8d8f025b..3da9b6ff 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -7,12 +7,15 @@ #include "../Literal.hpp" #include "../Result.hpp" #include "../dynamic/SelectFrom.hpp" +#include "Aggregation.hpp" +#include "AggregationOp.hpp" #include "As.hpp" #include "Col.hpp" #include "Operation.hpp" +#include "Operator.hpp" #include "Value.hpp" -#include "aggregations.hpp" #include "all_columns_exist.hpp" +#include "dynamic_aggregation_t.hpp" #include "dynamic_operator_t.hpp" #include "remove_nullable_t.hpp" #include "to_value.hpp" @@ -76,17 +79,18 @@ struct MakeField> { } }; -template -struct MakeField>> { +template +struct MakeField>> { static_assert(all_columns_exist>(), - "A column required in the AVG aggregation does not exist."); + "A column required in the aggregation does not exist."); static_assert( std::is_integral_v< remove_nullable_t>>> || std::is_floating_point_v< remove_nullable_t>>>, - "Values inside the AVG aggregation must be numerical."); + "Values inside the aggregation must be numerical."); static constexpr bool is_aggregation = true; static constexpr bool is_column = true; @@ -95,14 +99,15 @@ struct MakeField>> { using Type = rfl::field_type_t<_name, StructType>; dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{dynamic::Operation{ - .val = dynamic::Aggregation{dynamic::Aggregation::Avg{ - .val = dynamic::Column{.name = _name.str()}}}}}; + using DynamicAggregationType = dynamic_aggregation_t<_agg>; + return dynamic::SelectFrom::Field{ + dynamic::Operation{.val = dynamic::Aggregation{DynamicAggregationType{ + .val = dynamic::Column{.name = _name.str()}}}}}; } }; template -struct MakeField>> { +struct MakeField>> { static_assert(all_columns_exist>(), "A column required in the COUNT or COUNT_DISTINCT aggregation " "does not exist."); @@ -123,7 +128,7 @@ struct MakeField>> { }; template -struct MakeField> { +struct MakeField> { static constexpr bool is_aggregation = true; static constexpr bool is_column = true; @@ -138,67 +143,6 @@ struct MakeField> { } }; -template -struct MakeField>> { - static_assert(all_columns_exist>(), - "A column required in the MAX aggregation does not exist."); - - static constexpr bool is_aggregation = true; - static constexpr bool is_column = true; - - using Name = Literal<_name>; - using Type = rfl::field_type_t<_name, StructType>; - - dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{dynamic::Operation{ - .val = dynamic::Aggregation{dynamic::Aggregation::Max{ - .val = dynamic::Column{.name = _name.str()}}}}}; - } -}; - -template -struct MakeField>> { - static_assert(all_columns_exist>(), - "A column required in MIN aggregation does not exist."); - - static constexpr bool is_aggregation = true; - static constexpr bool is_column = true; - - using Name = Literal<_name>; - using Type = rfl::field_type_t<_name, StructType>; - - dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{dynamic::Operation{ - .val = dynamic::Aggregation{dynamic::Aggregation::Min{ - .val = dynamic::Column{.name = _name.str()}}}}}; - } -}; - -template -struct MakeField>> { - static_assert(all_columns_exist>(), - "A column required in SUM aggregation does not exist."); - - static_assert( - std::is_integral_v< - remove_nullable_t>>> || - std::is_floating_point_v< - remove_nullable_t>>>, - "Values inside the SUM aggregation must be numerical."); - - static constexpr bool is_aggregation = true; - static constexpr bool is_column = true; - - using Name = Literal<_name>; - using Type = rfl::field_type_t<_name, StructType>; - - dynamic::SelectFrom::Field operator()(const auto&) const { - return dynamic::SelectFrom::Field{dynamic::Operation{ - .val = dynamic::Aggregation{dynamic::Aggregation::Sum{ - .val = dynamic::Column{.name = _name.str()}}}}}; - } -}; - template struct MakeField> { From 8757c89b942a3d8059e7261f5bdcbcca75747680 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 15 Jun 2025 16:23:39 +0200 Subject: [PATCH 05/25] Added basic support for operations --- include/sqlgen/col.hpp | 168 ++++++++++-------- include/sqlgen/transpilation/Operation.hpp | 48 +++++ include/sqlgen/transpilation/make_field.hpp | 35 +++- include/sqlgen/transpilation/to_sql.hpp | 4 +- .../transpilation/to_transpilation_type.hpp | 48 +++++ tests/postgres/test_operations.cpp | 63 +++++++ tests/sqlite/test_operations.cpp | 55 ++++++ 7 files changed, 339 insertions(+), 82 deletions(-) create mode 100644 include/sqlgen/transpilation/to_transpilation_type.hpp create mode 100644 tests/postgres/test_operations.cpp create mode 100644 tests/sqlite/test_operations.cpp diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index e1a693ef..6d202ac0 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -8,9 +8,12 @@ #include "transpilation/Col.hpp" #include "transpilation/Condition.hpp" #include "transpilation/Desc.hpp" +#include "transpilation/Operation.hpp" +#include "transpilation/Operator.hpp" #include "transpilation/Set.hpp" #include "transpilation/Value.hpp" #include "transpilation/conditions.hpp" +#include "transpilation/to_transpilation_type.hpp" namespace sqlgen { @@ -77,93 +80,114 @@ struct Col { return transpilation::Set, std::string>{.to = _to}; } -}; -template -const auto col = Col<_name>{}; + template + friend auto operator==(const Col&, const T& _t) { + return transpilation::make_condition(transpilation::conditions::equal( + transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + } -template -auto operator"" _c() { - return Col<_name>{}; -} + template + friend auto operator!=(const Col&, const T& _t) { + return transpilation::make_condition(transpilation::conditions::not_equal( + transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + } -template -auto operator==(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::equal( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} + template + friend auto operator<(const Col&, const T& _t) { + return transpilation::make_condition(transpilation::conditions::lesser_than( + transpilation::Col<_name>{}, transpilation::to_transpilation_type(_t))); + } -template -auto operator==(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::equal( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); -} + template + friend auto operator<=(const Col&, const T& _t) { + return transpilation::make_condition( + transpilation::conditions::lesser_equal( + transpilation::Col<_name>{}, + transpilation::to_transpilation_type(_t))); + } -template -auto operator!=(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::not_equal( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} + template + friend auto operator>(const Col&, const T& _t) { + return transpilation::make_condition( + transpilation::conditions::greater_than( + transpilation::Col<_name>{}, + transpilation::to_transpilation_type(_t))); + } -template -auto operator!=(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::not_equal( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); -} + template + friend auto operator>=(const Col&, const T& _t) { + return transpilation::make_condition( + transpilation::conditions::greater_equal( + transpilation::Col<_name>{}, + transpilation::to_transpilation_type(_t))); + } -template -auto operator<(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::lesser_than( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} + 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)}; + } -template -auto operator<(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::lesser_than( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); -} + 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)}; + } -template -auto operator<=(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::lesser_equal( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} + 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)}; + } -template -auto operator<=(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::lesser_equal( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); -} + 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)}; + } +}; -template -auto operator>(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::greater_than( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} +template +const auto col = Col<_name>{}; -template -auto operator>(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::greater_than( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); +template +auto operator"" _c() { + return Col<_name>{}; } -template -auto operator>=(const Col<_name1>&, const Col<_name2>&) { - return transpilation::make_condition(transpilation::conditions::greater_equal( - transpilation::Col<_name1>{}, transpilation::Col<_name2>{})); -} +namespace transpilation { -template -auto operator>=(const Col<_name1>&, const T& _t) { - return transpilation::make_condition(transpilation::conditions::greater_equal( - transpilation::Col<_name1>{}, transpilation::make_value(_t))); -} +template +struct ToTranspilationType> { + using Type = transpilation::Col<_name>; + + Type operator()(const auto&) const noexcept { + return transpilation::Col<_name>{}; + } +}; + +} // namespace transpilation } // namespace sqlgen diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index fc574859..a7302fde 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -6,6 +6,7 @@ #include "../Result.hpp" #include "Operator.hpp" +#include "to_transpilation_type.hpp" namespace sqlgen::transpilation { @@ -18,6 +19,53 @@ struct Operation { Operand1Type operand1; Operand2Type operand2; + + 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)}; + } + + 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)}; + } + + 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)}; + } + + 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)}; + } +}; + +template +struct ToTranspilationType> { + using Type = Operation<_op, _Operand1Type, _Operand2Type>; + + Type operator()(const Type& _val) const noexcept { return _val; } }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 3da9b6ff..02a04ee8 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -26,13 +26,13 @@ namespace sqlgen::transpilation { template struct MakeField; -template +template struct MakeField { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; using Name = Nothing; - using Type = ValueType; + using Type = std::remove_cvref_t; dynamic::SelectFrom::Field operator()(const auto& _val) const { return dynamic::SelectFrom::Field{ @@ -57,6 +57,20 @@ struct MakeField> { } }; +template +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = std::remove_cvref_t; + + dynamic::SelectFrom::Field operator()(const auto& _val) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{.val = to_value(_val.val)}}; + } +}; + template struct MakeField> { @@ -150,18 +164,23 @@ struct MakeField> { static constexpr bool is_column = false; using Name = Nothing; + using Type = + typename MakeField>:: + Type; // TODO: Better solution dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; constexpr auto num_operands = num_operands_v<_op>; if constexpr (num_operands == 2) { return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = MakeField>{}( - _o.operand1) - .val, - .op2 = MakeField>{}( - _o.operand2) - .val}}}; + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .op2 = Ref::make( + MakeField>{}( + _o.operand2) + .val)}}}; } } }; diff --git a/include/sqlgen/transpilation/to_sql.hpp b/include/sqlgen/transpilation/to_sql.hpp index b4d7ccab..a84309de 100644 --- a/include/sqlgen/transpilation/to_sql.hpp +++ b/include/sqlgen/transpilation/to_sql.hpp @@ -77,9 +77,9 @@ struct ToSQL> { }; template + class GroupByType, class OrderByType, class LimitType, class ToType> struct ToSQL> { + OrderByType, LimitType, ToType>> { dynamic::Statement operator()(const auto& _select_from) const { return to_select_from( diff --git a/include/sqlgen/transpilation/to_transpilation_type.hpp b/include/sqlgen/transpilation/to_transpilation_type.hpp new file mode 100644 index 00000000..99e3021d --- /dev/null +++ b/include/sqlgen/transpilation/to_transpilation_type.hpp @@ -0,0 +1,48 @@ +#ifndef SQLGEN_TRANSPILATION_TO_TRANSPILATION_TYPE_HPP_ +#define SQLGEN_TRANSPILATION_TO_TRANSPILATION_TYPE_HPP_ + +#include +#include + +#include "Aggregation.hpp" +#include "AggregationOp.hpp" +#include "Value.hpp" + +namespace sqlgen::transpilation { + +template +struct ToTranspilationType; + +template +struct ToTranspilationType { + using Type = Value; + + Type operator()(const T& _val) const noexcept { return make_value(_val); } +}; + +template <> +struct ToTranspilationType { + using Type = Value; + + Type operator()(const char* _val) const noexcept { return make_value(_val); } +}; + +template +struct ToTranspilationType> { + using Type = Aggregation<_agg, _ValueType>; + + Type operator()(const Type& _val) const noexcept { return _val; } +}; + +template +auto to_transpilation_type(const T& _t) { + return ToTranspilationType>{}(_t); +} + +inline auto to_transpilation_type(const char* _t) { + return ToTranspilationType{}(_t); +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp new file mode 100644 index 00000000..8714981b --- /dev/null +++ b/tests/postgres/test_operations.cpp @@ -0,0 +1,63 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + double age; +}; + +TEST(postgres, test_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + struct Children { + int id_plus_age; + int age_times_2; + int id_plus_2_minus_age; + }; + + const auto get_children = + select_from(("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + where("age"_c < 18) | to>; + + const auto children = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + const std::string expected = + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + + EXPECT_EQ(rfl::json::write(children), expected); +} + +} // namespace test_operations + +#endif diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp new file mode 100644 index 00000000..e05d2c90 --- /dev/null +++ b/tests/sqlite/test_operations.cpp @@ -0,0 +1,55 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + using namespace sqlgen; + + struct Children { + int id_plus_age; + int age_times_2; + int id_plus_2_minus_age; + }; + + const auto get_children = + select_from(("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + where("age"_c < 18) | to>; + + const auto children = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + const std::string expected = + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + + EXPECT_EQ(rfl::json::write(children), expected); +} + +} // namespace test_operations + From 54f6e0afd87c5d250445465a68f703f759aca5ed Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 15 Jun 2025 17:40:21 +0200 Subject: [PATCH 06/25] Better type inference for operators --- include/sqlgen/transpilation/make_field.hpp | 25 ++++++++----------- include/sqlgen/transpilation/underlying_t.hpp | 21 ++++++++++++++++ tests/sqlite/test_operations.cpp | 6 ++--- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 02a04ee8..6b71e3ab 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -159,29 +159,26 @@ struct MakeField> { template + requires((num_operands_v<_op>) == 2) struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; using Name = Nothing; using Type = - typename MakeField>:: - Type; // TODO: Better solution + underlying_t>; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; - constexpr auto num_operands = num_operands_v<_op>; - if constexpr (num_operands == 2) { - return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; - } + return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .op2 = Ref::make( + MakeField>{}( + _o.operand2) + .val)}}}; } }; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 92f598e6..18542d6e 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -6,7 +6,9 @@ #include "Col.hpp" #include "Desc.hpp" +#include "Operation.hpp" #include "Value.hpp" +#include "dynamic_operator_t.hpp" #include "remove_reflection_t.hpp" namespace sqlgen::transpilation { @@ -24,6 +26,25 @@ struct Underlying>> { using Type = remove_reflection_t>; }; +template + requires((num_operands_v<_op>) == 2) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + using Underlying2 = + typename Underlying>::Type; + + static_assert( + requires(Underlying1 op1, Underlying2 op2) { op1 + op2; }, + "Binary operations are not possible on these types."); + + using Type = + std::invoke_result_t; +}; + template struct Underlying> { using Type = _Type; diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index e05d2c90..ac0b2704 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -31,13 +31,13 @@ TEST(sqlite, test_operations) { struct Children { int id_plus_age; int age_times_2; - int id_plus_2_minus_age; + int age_plus_2_minus_id; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + ("age"_c + 2 - "id"_c) | as<"age_plus_2_minus_id">) | where("age"_c < 18) | to>; const auto children = sqlite::connect() @@ -46,7 +46,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 3d95a378aa88aa35bcc9ff406e65ceb68603ee37 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 15 Jun 2025 22:24:20 +0200 Subject: [PATCH 07/25] Allow complex operations inside conditions --- include/sqlgen/dynamic/Condition.hpp | 33 +-- include/sqlgen/transpilation/Operation.hpp | 56 +++++ include/sqlgen/transpilation/to_condition.hpp | 237 +++++------------- include/sqlgen/transpilation/underlying_t.hpp | 2 + src/sqlgen/postgres/to_sql.cpp | 47 ++-- src/sqlgen/sqlite/to_sql.cpp | 47 ++-- tests/postgres/test_where_with_operations.cpp | 57 +++++ tests/sqlite/test_where_with_operations.cpp | 47 ++++ 8 files changed, 282 insertions(+), 244 deletions(-) create mode 100644 tests/postgres/test_where_with_operations.cpp create mode 100644 tests/sqlite/test_where_with_operations.cpp diff --git a/include/sqlgen/dynamic/Condition.hpp b/include/sqlgen/dynamic/Condition.hpp index 9ffd2a9d..3cea3b52 100644 --- a/include/sqlgen/dynamic/Condition.hpp +++ b/include/sqlgen/dynamic/Condition.hpp @@ -6,6 +6,7 @@ #include "../Ref.hpp" #include "Column.hpp" #include "ColumnOrValue.hpp" +#include "Operation.hpp" namespace sqlgen::dynamic { @@ -16,50 +17,50 @@ struct Condition { }; struct Equal { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct GreaterEqual { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct GreaterThan { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct IsNotNull { - Column op; + Operation op; }; struct IsNull { - Column op; + Operation op; }; struct LesserEqual { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct LesserThan { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct Like { - Column op; + Operation op; dynamic::Value pattern; }; struct NotEqual { - Column op1; - ColumnOrValue op2; + Operation op1; + Operation op2; }; struct NotLike { - Column op; + Operation op; dynamic::Value pattern; }; diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index a7302fde..effd76d8 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -5,7 +5,9 @@ #include #include "../Result.hpp" +#include "Condition.hpp" #include "Operator.hpp" +#include "conditions.hpp" #include "to_transpilation_type.hpp" namespace sqlgen::transpilation { @@ -20,6 +22,60 @@ struct Operation { Operand1Type operand1; Operand2Type operand2; + /// Returns an IS NULL condition. + auto is_null() const noexcept { + return make_condition(conditions::is_null(*this)); + } + + /// Returns a IS NOT NULL condition. + auto is_not_null() const noexcept { + return make_condition(conditions::is_not_null(*this)); + } + + /// Returns a LIKE condition. + auto like(const std::string& _pattern) const noexcept { + return make_condition(conditions::like(*this, _pattern)); + } + + /// Returns a NOT LIKE condition. + auto not_like(const std::string& _pattern) const noexcept { + return make_condition(conditions::not_like(*this, _pattern)); + } + + template + friend auto operator==(const Operation& _o, const T& _t) { + return make_condition(conditions::equal(_o, to_transpilation_type(_t))); + } + + template + friend auto operator!=(const Operation& _o, const T& _t) { + return make_condition(conditions::not_equal(_o, to_transpilation_type(_t))); + } + + template + friend auto operator<(const Operation& _o, const T& _t) { + return make_condition( + conditions::lesser_than(_o, to_transpilation_type(_t))); + } + + template + friend auto operator<=(const Operation& _o, const T& _t) { + return make_condition( + conditions::lesser_equal(_o, to_transpilation_type(_t))); + } + + template + friend auto operator>(const Operation& _o, const T& _t) { + return make_condition( + conditions::greater_than(_o, to_transpilation_type(_t))); + } + + template + friend auto operator>=(const Operation& _o, const T& _t) { + return make_condition( + conditions::greater_equal(_o, to_transpilation_type(_t))); + } + template friend auto operator/(const Operation& _op1, const T& _op2) noexcept { using OtherType = typename transpilation::ToTranspilationType< diff --git a/include/sqlgen/transpilation/to_condition.hpp b/include/sqlgen/transpilation/to_condition.hpp index c405a700..3d452c21 100644 --- a/include/sqlgen/transpilation/to_condition.hpp +++ b/include/sqlgen/transpilation/to_condition.hpp @@ -12,7 +12,8 @@ #include "Condition.hpp" #include "all_columns_exist.hpp" #include "conditions.hpp" -#include "to_value.hpp" +#include "make_field.hpp" +#include "to_transpilation_type.hpp" #include "underlying_t.hpp" namespace sqlgen::transpilation { @@ -40,245 +41,125 @@ struct ToCondition> { } }; -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::equality_comparable_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::equality_comparable_with, + underlying_t>, "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::Equal{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; - } -}; - -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::equality_comparable_with>, - underlying_t>>, - "Must be equality comparable."); - - dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::Equal{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; - } -}; - -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, - "Must be totally ordered."); - - dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::GreaterEqual{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; + return dynamic::Condition{ + .val = dynamic::Condition::Equal{.op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::totally_ordered_with, + underlying_t>, "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::GreaterEqual{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; - } -}; - -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, - "Must be totally ordered."); - - dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::GreaterThan{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; + .op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::totally_ordered_with, + underlying_t>, "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::GreaterThan{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; + .op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::totally_ordered_with, + underlying_t>, "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::LesserEqual{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; + .op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, - "Must be totally ordered."); - - dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::LesserEqual{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; - } -}; - -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, - "Must be totally ordered."); - - dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::LesserThan{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; - } -}; - -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::totally_ordered_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::totally_ordered_with, + underlying_t>, "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::LesserThan{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; + .op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition>> { - static_assert(all_columns_exist>(), "All columns must exist."); +template +struct ToCondition> { static_assert( - std::equality_comparable_with>, + std::equality_comparable_with, underlying_t>>, "Must be equality comparable with a string."); dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::Like{ - .op = dynamic::Column{.name = _name.str()}, - .pattern = to_value(_cond.pattern)}}; + return dynamic::Condition{ + .val = dynamic::Condition::Like{.op = make_field(_cond.op).val, + .pattern = to_value(_cond.pattern)}}; } }; -template -struct ToCondition>> { - static_assert(all_columns_exist>(), "All columns must exist."); - - dynamic::Condition operator()(const auto&) const { +template +struct ToCondition> { + dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::IsNotNull{ - .op = dynamic::Column{.name = _name.str()}}}; - } -}; - -template -struct ToCondition>> { - static_assert(all_columns_exist>(), "All columns must exist."); - - dynamic::Condition operator()(const auto&) const { - return dynamic::Condition{.val = dynamic::Condition::IsNull{ - .op = dynamic::Column{.name = _name.str()}}}; + .op = make_field(_cond.op).val}}; } }; -template -struct ToCondition, Col<_name2>>> { - static_assert(all_columns_exist, Col<_name2>>(), - "All columns must exist."); - static_assert(std::equality_comparable_with>, - underlying_t>>, - "Must be equality comparable."); - +template +struct ToCondition> { dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::NotEqual{ - .op1 = dynamic::Column{.name = _name1.str()}, - .op2 = dynamic::Column{.name = _name2.str()}, - }}; + return dynamic::Condition{ + .val = dynamic::Condition::IsNull{.op = make_field(_cond.op).val}}; } }; -template -struct ToCondition, Value>> { - static_assert(all_columns_exist>(), "All columns must exist."); - static_assert(std::equality_comparable_with>, - underlying_t>>, +template +struct ToCondition> { + static_assert(std::equality_comparable_with, + underlying_t>, "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { return dynamic::Condition{.val = dynamic::Condition::NotEqual{ - .op1 = dynamic::Column{.name = _name.str()}, - .op2 = to_value(_cond.op2.val), - }}; + .op1 = make_field(_cond.op1).val, + .op2 = make_field(_cond.op2).val}}; } }; -template -struct ToCondition>> { - static_assert(all_columns_exist>(), "All columns must exist."); +template +struct ToCondition> { static_assert( - std::equality_comparable_with>, + std::equality_comparable_with, underlying_t>>, "Must be equality comparable with a string."); dynamic::Condition operator()(const auto& _cond) const { - return dynamic::Condition{.val = dynamic::Condition::NotLike{ - .op = dynamic::Column{.name = _name.str()}, - .pattern = to_value(_cond.pattern)}}; + return dynamic::Condition{ + .val = dynamic::Condition::NotLike{.op = make_field(_cond.op).val, + .pattern = to_value(_cond.pattern)}}; } }; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 18542d6e..4968bb6e 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -8,6 +8,7 @@ #include "Desc.hpp" #include "Operation.hpp" #include "Value.hpp" +#include "all_columns_exist.hpp" #include "dynamic_operator_t.hpp" #include "remove_reflection_t.hpp" @@ -18,6 +19,7 @@ struct Underlying; template struct Underlying> { + static_assert(all_columns_exist>(), "All columns must exist."); using Type = remove_reflection_t>; }; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 7fe1bb94..97551fa2 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -128,59 +128,56 @@ std::string condition_to_sql(const dynamic::Condition& _cond) noexcept { template std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { using C = std::remove_cvref_t; - std::stringstream stream; if constexpr (std::is_same_v) { - stream << "(" << condition_to_sql(*_condition.cond1) << ") AND (" - << condition_to_sql(*_condition.cond2) << ")"; + return std::format("({}) AND ({})", condition_to_sql(*_condition.cond1), + condition_to_sql(*_condition.cond2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " = " - << column_or_value_to_sql(_condition.op2); + return std::format("{} = {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " >= " << column_or_value_to_sql(_condition.op2); + return std::format("{} >= {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " > " - << column_or_value_to_sql(_condition.op2); + return std::format("{} > {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " IS NULL"; + return std::format("{} IS NULL", operation_to_sql(_condition.op)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " IS NOT NULL"; + return std::format("{} IS NOT NULL", operation_to_sql(_condition.op)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " <= " << column_or_value_to_sql(_condition.op2); + return std::format("{} <= {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " < " - << column_or_value_to_sql(_condition.op2); + return std::format("{} < {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " LIKE " - << column_or_value_to_sql(_condition.pattern); + return std::format("{} LIKE {}", operation_to_sql(_condition.op), + column_or_value_to_sql(_condition.pattern)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " != " << column_or_value_to_sql(_condition.op2); + return std::format("{} != {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " NOT LIKE " - << column_or_value_to_sql(_condition.pattern); + return std::format("{} NOT LIKE {}", operation_to_sql(_condition.op), + column_or_value_to_sql(_condition.pattern)); } else if constexpr (std::is_same_v) { - stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" - << condition_to_sql(*_condition.cond2) << ")"; + return std::format("({}) OR ({})", condition_to_sql(*_condition.cond1), + condition_to_sql(*_condition.cond2)); } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } - - return stream.str(); } std::string column_to_sql_definition(const dynamic::Column& _col) noexcept { diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 64f47f94..01f4875e 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -112,59 +112,56 @@ std::string condition_to_sql(const dynamic::Condition& _cond) noexcept { template std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { using C = std::remove_cvref_t; - std::stringstream stream; if constexpr (std::is_same_v) { - stream << "(" << condition_to_sql(*_condition.cond1) << ") AND (" - << condition_to_sql(*_condition.cond2) << ")"; + return std::format("({}) AND ({})", condition_to_sql(*_condition.cond1), + condition_to_sql(*_condition.cond2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " = " - << column_or_value_to_sql(_condition.op2); + return std::format("{} = {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " >= " << column_or_value_to_sql(_condition.op2); + return std::format("{} >= {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " > " - << column_or_value_to_sql(_condition.op2); + return std::format("{} > {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " IS NULL"; + return std::format("{} IS NULL", operation_to_sql(_condition.op)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " IS NOT NULL"; + return std::format("{} IS NOT NULL", operation_to_sql(_condition.op)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " <= " << column_or_value_to_sql(_condition.op2); + return std::format("{} <= {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) << " < " - << column_or_value_to_sql(_condition.op2); + return std::format("{} < {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " LIKE " - << column_or_value_to_sql(_condition.pattern); + return std::format("{} LIKE {}", operation_to_sql(_condition.op), + column_or_value_to_sql(_condition.pattern)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op1) - << " != " << column_or_value_to_sql(_condition.op2); + return std::format("{} != {}", operation_to_sql(_condition.op1), + operation_to_sql(_condition.op2)); } else if constexpr (std::is_same_v) { - stream << column_or_value_to_sql(_condition.op) << " NOT LIKE " - << column_or_value_to_sql(_condition.pattern); + return std::format("{} NOT LIKE {}", operation_to_sql(_condition.op), + column_or_value_to_sql(_condition.pattern)); } else if constexpr (std::is_same_v) { - stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" - << condition_to_sql(*_condition.cond2) << ")"; + return std::format("({}) OR ({})", condition_to_sql(*_condition.cond1), + condition_to_sql(*_condition.cond2)); } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } - - return stream.str(); } std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept { diff --git a/tests/postgres/test_where_with_operations.cpp b/tests/postgres/test_where_with_operations.cpp new file mode 100644 index 00000000..b24f39fc --- /dev/null +++ b/tests/postgres/test_where_with_operations.cpp @@ -0,0 +1,57 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_where_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_where_with_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1).value(); + + const auto query = sqlgen::read> | + where("age"_c * 2 + 4 < 40 and "first_name"_c != "Hugo") | + order_by("age"_c); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_operations + +#endif diff --git a/tests/sqlite/test_where_with_operations.cpp b/tests/sqlite/test_where_with_operations.cpp new file mode 100644 index 00000000..f4eaf77a --- /dev/null +++ b/tests/sqlite/test_where_with_operations.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_where_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_where_with_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + + const auto query = sqlgen::read> | + where("age"_c * 2 + 4 < 40 and "first_name"_c != "Hugo") | + order_by("age"_c); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_operations From c1f33cb46c6ea2fe0171c3eb6082e1c0dc7f69fb Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 16 Jun 2025 22:29:07 +0200 Subject: [PATCH 08/25] Made sure that nullable types are handled correctly --- include/sqlgen/transpilation/underlying_t.hpp | 17 +++-- .../test_operations_with_nullable.cpp | 63 +++++++++++++++++++ .../sqlite/test_operations_with_nullable.cpp | 56 +++++++++++++++++ .../test_where_with_nullable_operations.cpp | 48 ++++++++++++++ 4 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 tests/postgres/test_operations_with_nullable.cpp create mode 100644 tests/sqlite/test_operations_with_nullable.cpp create mode 100644 tests/sqlite/test_where_with_nullable_operations.cpp diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 4968bb6e..2cb93b69 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -10,6 +10,8 @@ #include "Value.hpp" #include "all_columns_exist.hpp" #include "dynamic_operator_t.hpp" +#include "is_nullable.hpp" +#include "remove_nullable_t.hpp" #include "remove_reflection_t.hpp" namespace sqlgen::transpilation { @@ -37,14 +39,17 @@ struct Underlying> { typename Underlying>::Type; static_assert( - requires(Underlying1 op1, Underlying2 op2) { op1 + op2; }, + requires(remove_nullable_t op1, + remove_nullable_t op2) { op1 + op2; }, "Binary operations are not possible on these types."); - using Type = - std::invoke_result_t; + using ResultType = std::invoke_result_t< + decltype([](const auto& op1, const auto& op2) { return op1 + op2; }), + remove_nullable_t, remove_nullable_t>; + + using Type = std::conditional_t || + is_nullable_v, + std::optional, ResultType>; }; template diff --git a/tests/postgres/test_operations_with_nullable.cpp b/tests/postgres/test_operations_with_nullable.cpp new file mode 100644 index 00000000..7ea50995 --- /dev/null +++ b/tests/postgres/test_operations_with_nullable.cpp @@ -0,0 +1,63 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_operations_with_nullable { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(postgres, test_operations_with_nullable) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + struct Children { + std::optional id_plus_age; + std::optional age_times_2; + std::optional id_plus_2_minus_age; + }; + + const auto get_children = + select_from(("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + where("age"_c < 18) | to>; + + const auto children = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + const std::string expected = + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + + EXPECT_EQ(rfl::json::write(children), expected); +} + +} // namespace test_operations_with_nullable + +#endif diff --git a/tests/sqlite/test_operations_with_nullable.cpp b/tests/sqlite/test_operations_with_nullable.cpp new file mode 100644 index 00000000..b3267826 --- /dev/null +++ b/tests/sqlite/test_operations_with_nullable.cpp @@ -0,0 +1,56 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_operations_with_nullable { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(sqlite, test_operations_with_nullable) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + using namespace sqlgen; + + struct Children { + std::optional id_plus_age; + std::optional age_times_2; + std::optional age_plus_2_minus_id; + }; + + const auto get_children = + select_from(("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("age"_c + 2 - "id"_c) | as<"age_plus_2_minus_id">) | + where("age"_c < 18) | to>; + + const auto children = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + const std::string expected = + R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1}])"; + + EXPECT_EQ(rfl::json::write(children), expected); +} + +} // namespace test_operations_with_nullable + diff --git a/tests/sqlite/test_where_with_nullable_operations.cpp b/tests/sqlite/test_where_with_nullable_operations.cpp new file mode 100644 index 00000000..38bc22d4 --- /dev/null +++ b/tests/sqlite/test_where_with_nullable_operations.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_nullable_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(sqlite, test_where_with_nullable_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + + const auto query = sqlgen::read> | + where("age"_c * 2 + 4 < 40 and "first_name"_c != "Hugo") | + order_by("age"_c); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_nullable_operations From 42ccd208ed86411f757ed4bfe77ef6d949c8b052 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 17 Jun 2025 22:00:50 +0200 Subject: [PATCH 09/25] Added the modulo operator --- include/sqlgen/col.hpp | 11 +++++++++++ include/sqlgen/dynamic/Operation.hpp | 7 ++++++- include/sqlgen/transpilation/Operation.hpp | 10 ++++++++++ include/sqlgen/transpilation/Operator.hpp | 2 +- include/sqlgen/transpilation/OperatorCategory.hpp | 10 ++++++++++ .../sqlgen/transpilation/dynamic_operator_t.hpp | 15 +++++++++++++++ include/sqlgen/transpilation/make_field.hpp | 4 +++- include/sqlgen/transpilation/underlying_t.hpp | 3 ++- src/sqlgen/postgres/to_sql.cpp | 4 ++++ src/sqlgen/sqlite/to_sql.cpp | 4 ++++ tests/postgres/test_operations.cpp | 9 ++++++--- tests/sqlite/test_operations.cpp | 4 +++- vcpkg.json | 3 ++- 13 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 include/sqlgen/transpilation/OperatorCategory.hpp diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index 6d202ac0..5be2e4cd 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -145,6 +145,17 @@ struct Col { .operand2 = transpilation::to_transpilation_type(_op2)}; } + 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)}; + } + template friend auto operator*(const Col&, const T& _op2) noexcept { using OtherType = typename transpilation::ToTranspilationType< diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index a473ba46..d9148e59 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -22,6 +22,11 @@ struct Operation { Ref op2; }; + struct Mod { + Ref op1; + Ref op2; + }; + struct Multiplies { Ref op1; Ref op2; @@ -33,7 +38,7 @@ struct Operation { }; using ReflectionType = rfl::TaggedUnion<"what", Aggregation, Column, Divides, - Minus, Multiplies, Plus, Value>; + Minus, Mod, Multiplies, Plus, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index effd76d8..05ad05b1 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -96,6 +96,16 @@ struct Operation { .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } + 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)}; + } + template friend auto operator*(const Operation& _op1, const T& _op2) noexcept { using OtherType = typename transpilation::ToTranspilationType< diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 805b992e..7fb7e39b 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -3,7 +3,7 @@ namespace sqlgen::transpilation { -enum class Operator { divides, minus, multiplies, plus }; +enum class Operator { divides, minus, mod, multiplies, plus }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/OperatorCategory.hpp b/include/sqlgen/transpilation/OperatorCategory.hpp new file mode 100644 index 00000000..0311c0ca --- /dev/null +++ b/include/sqlgen/transpilation/OperatorCategory.hpp @@ -0,0 +1,10 @@ +#ifndef SQLGEN_TRANSPILATION_OPERATORCATEGORY_HPP_ +#define SQLGEN_TRANSPILATION_OPERATORCATEGORY_HPP_ + +namespace sqlgen::transpilation { + +enum class OperatorCategory { numerical, string }; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index d36be821..556498a6 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -3,6 +3,7 @@ #include "../dynamic/Operation.hpp" #include "Operator.hpp" +#include "OperatorCategory.hpp" namespace sqlgen::transpilation { @@ -12,24 +13,35 @@ struct DynamicOperator; template <> struct DynamicOperator { static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::numerical; using Type = dynamic::Operation::Divides; }; template <> struct DynamicOperator { static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::numerical; using Type = dynamic::Operation::Minus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Mod; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::numerical; using Type = dynamic::Operation::Multiplies; }; template <> struct DynamicOperator { static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::numerical; using Type = dynamic::Operation::Plus; }; @@ -39,6 +51,9 @@ using dynamic_operator_t = typename DynamicOperator::Type; template inline constexpr size_t num_operands_v = DynamicOperator::num_operands; +template +inline constexpr auto operator_category_v = DynamicOperator::category; + } // namespace sqlgen::transpilation #endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 6b71e3ab..73a1c30e 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -13,6 +13,7 @@ #include "Col.hpp" #include "Operation.hpp" #include "Operator.hpp" +#include "OperatorCategory.hpp" #include "Value.hpp" #include "all_columns_exist.hpp" #include "dynamic_aggregation_t.hpp" @@ -159,7 +160,8 @@ struct MakeField> { template - requires((num_operands_v<_op>) == 2) + requires((num_operands_v<_op>) == 2 && + (operator_category_v<_op>) == OperatorCategory::numerical) struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 2cb93b69..14e8118d 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -31,7 +31,8 @@ struct Underlying>> { }; template - requires((num_operands_v<_op>) == 2) + requires((num_operands_v<_op>) == 2 && + (operator_category_v<_op>) == OperatorCategory::numerical) struct Underlying> { using Underlying1 = typename Underlying>::Type; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 97551fa2..11c9dffe 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -375,6 +375,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("mod({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("({}) * ({})", 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 01f4875e..40e17058 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -335,6 +335,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("mod({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("({}) * ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index 8714981b..26a88e9e 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -15,7 +15,7 @@ struct Person { sqlgen::PrimaryKey id; std::string first_name; std::string last_name; - double age; + int age; }; TEST(postgres, test_operations) { @@ -38,13 +38,16 @@ TEST(postgres, test_operations) { int id_plus_age; int age_times_2; int id_plus_2_minus_age; + int age_mod_3; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, + ("age"_c % 3) | as<"age_mod_3">, ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | - where("age"_c < 18) | to>; + where("age"_c < 18) | order_by("age"_c.desc()) | + to>; const auto children = postgres::connect(credentials) .and_then(drop | if_exists) @@ -53,7 +56,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index ac0b2704..44bb309d 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -32,11 +32,13 @@ TEST(sqlite, test_operations) { int id_plus_age; int age_times_2; int age_plus_2_minus_id; + int age_mod_3; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, + ("age"_c % 3) | as<"age_mod_3">, ("age"_c + 2 - "id"_c) | as<"age_plus_2_minus_id">) | where("age"_c < 18) | to>; @@ -46,7 +48,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1}])"; + R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11,"age_mod_3":1},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8,"age_mod_3":2},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1,"age_mod_3":0}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/vcpkg.json b/vcpkg.json index 54057197..dadc80a3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -20,7 +20,8 @@ "dependencies": [ { "name": "sqlite3", - "version>=": "3.49.1" + "version>=": "3.49.1", + "features": ["math"] } ] }, From 4bb8faec2ebdc27da951a4cdcaab70b65d2e58dd Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 17 Jun 2025 23:47:25 +0200 Subject: [PATCH 10/25] Added the abs operation --- include/sqlgen.hpp | 1 + include/sqlgen/dynamic/Operation.hpp | 9 +++++++-- include/sqlgen/operations.hpp | 20 +++++++++++++++++++ include/sqlgen/transpilation/Operator.hpp | 2 +- .../transpilation/dynamic_operator_t.hpp | 7 +++++++ include/sqlgen/transpilation/make_field.hpp | 20 +++++++++++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 14 +++++++++++++ src/sqlgen/postgres/to_sql.cpp | 5 ++++- src/sqlgen/sqlite/to_sql.cpp | 5 ++++- tests/postgres/test_operations.cpp | 4 +++- tests/sqlite/test_operations.cpp | 11 ++++++---- 11 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 include/sqlgen/operations.hpp diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index dbb53e26..ed75e198 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -30,6 +30,7 @@ #include "sqlgen/insert.hpp" #include "sqlgen/is_connection.hpp" #include "sqlgen/limit.hpp" +#include "sqlgen/operations.hpp" #include "sqlgen/order_by.hpp" #include "sqlgen/patterns.hpp" #include "sqlgen/read.hpp" diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index d9148e59..876e555b 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -12,6 +12,10 @@ namespace sqlgen::dynamic { struct Operation { + struct Abs { + Ref op1; + }; + struct Divides { Ref op1; Ref op2; @@ -37,8 +41,9 @@ struct Operation { Ref op2; }; - using ReflectionType = rfl::TaggedUnion<"what", Aggregation, Column, Divides, - Minus, Mod, Multiplies, Plus, Value>; + using ReflectionType = + rfl::TaggedUnion<"what", Abs, Aggregation, Column, Divides, Minus, Mod, + Multiplies, Plus, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp new file mode 100644 index 00000000..7d75fe11 --- /dev/null +++ b/include/sqlgen/operations.hpp @@ -0,0 +1,20 @@ +#ifndef SQLGEN_OPERATIONS_HPP_ +#define SQLGEN_OPERATIONS_HPP_ + +#include +#include + +#include "transpilation/Operation.hpp" +#include "transpilation/Operator.hpp" + +namespace sqlgen { + +template +auto abs(const T& _t) { + return transpilation::Operation>{.operand1 = _t}; +} + +} // namespace sqlgen + +#endif diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 7fb7e39b..4fdfbd90 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -3,7 +3,7 @@ namespace sqlgen::transpilation { -enum class Operator { divides, minus, mod, multiplies, plus }; +enum class Operator { abs, divides, minus, mod, multiplies, plus }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 556498a6..d01b9720 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -10,6 +10,13 @@ namespace sqlgen::transpilation { template struct DynamicOperator; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Abs; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 73a1c30e..9b0db774 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -158,6 +158,26 @@ struct MakeField> { } }; +template + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::numerical) +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = underlying_t>; + + 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)}}}; + } +}; + template requires((num_operands_v<_op>) == 2 && diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 14e8118d..4271e3fb 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -30,6 +30,20 @@ struct Underlying>> { using Type = remove_reflection_t>; }; +template + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::numerical) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + + static_assert(std::is_integral_v> || + std::is_floating_point_v>, + "Must be a numerical type"); + + using Type = Underlying1; +}; + template requires((num_operands_v<_op>) == 2 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 11c9dffe..da1ee981 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -361,7 +361,10 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return std::format("abs({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 40e17058..a1604128 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -321,7 +321,10 @@ std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept { std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return std::format("abs({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index 26a88e9e..5528b3fa 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -39,12 +39,14 @@ TEST(postgres, test_operations) { int age_times_2; int id_plus_2_minus_age; int age_mod_3; + int abs_age; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, + abs("age"_c * (-1)) | as<"abs_age">, ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -56,7 +58,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index 44bb309d..8a16811c 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -31,16 +31,19 @@ TEST(sqlite, test_operations) { struct Children { int id_plus_age; int age_times_2; - int age_plus_2_minus_id; + int id_plus_2_minus_age; int age_mod_3; + int abs_age; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, - ("age"_c + 2 - "id"_c) | as<"age_plus_2_minus_id">) | - where("age"_c < 18) | to>; + abs("age"_c * (-1)) | as<"abs_age">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + where("age"_c < 18) | order_by("age"_c.desc()) | + to>; const auto children = sqlite::connect() .and_then(write(std::ref(people1))) @@ -48,7 +51,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11,"age_mod_3":1},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8,"age_mod_3":2},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1,"age_mod_3":0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0}])"; EXPECT_EQ(rfl::json::write(children), expected); } From ae4ae3666772362394da265b4704766fbd61df83 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 18 Jun 2025 00:47:14 +0200 Subject: [PATCH 11/25] Added more operations --- include/sqlgen/dynamic/Operation.hpp | 42 +++++++++- include/sqlgen/operations.hpp | 80 ++++++++++++++++++- include/sqlgen/transpilation/Operator.hpp | 18 ++++- .../transpilation/dynamic_operator_t.hpp | 63 +++++++++++++++ src/sqlgen/postgres/to_sql.cpp | 27 +++++++ src/sqlgen/sqlite/to_sql.cpp | 27 +++++++ tests/sqlite/test_operations.cpp | 15 ++-- 7 files changed, 260 insertions(+), 12 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 876e555b..49bc6799 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -6,7 +6,6 @@ #include "../Ref.hpp" #include "Aggregation.hpp" #include "Column.hpp" -#include "ColumnOrValue.hpp" #include "Value.hpp" namespace sqlgen::dynamic { @@ -16,11 +15,35 @@ struct Operation { Ref op1; }; + struct Ceil { + Ref op1; + }; + + struct Cos { + Ref op1; + }; + struct Divides { Ref op1; Ref op2; }; + struct Exp { + Ref op1; + }; + + struct Floor { + Ref op1; + }; + + struct Ln { + Ref op1; + }; + + struct Log2 { + Ref op1; + }; + struct Minus { Ref op1; Ref op2; @@ -41,9 +64,22 @@ struct Operation { Ref op2; }; + struct Sin { + Ref op1; + }; + + struct Sqrt { + Ref op1; + }; + + struct Tan { + Ref op1; + }; + using ReflectionType = - rfl::TaggedUnion<"what", Abs, Aggregation, Column, Divides, Minus, Mod, - Multiplies, Plus, Value>; + rfl::TaggedUnion<"what", Abs, Aggregation, Ceil, Column, Cos, Divides, + Exp, Floor, Ln, Log2, Minus, Mod, Multiplies, Plus, Sin, + Sqrt, Tan, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index 7d75fe11..f64d3660 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -4,15 +4,91 @@ #include #include +#include "col.hpp" #include "transpilation/Operation.hpp" #include "transpilation/Operator.hpp" +#include "transpilation/to_transpilation_type.hpp" namespace sqlgen { template auto abs(const T& _t) { - return transpilation::Operation>{.operand1 = _t}; + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto ceil(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto cos(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto exp(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto floor(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto ln(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto log2(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 = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto sqrt(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto tan(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; } } // namespace sqlgen diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 4fdfbd90..7e397350 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -3,7 +3,23 @@ namespace sqlgen::transpilation { -enum class Operator { abs, divides, minus, mod, multiplies, plus }; +enum class Operator { + abs, + ceil, + cos, + exp, + floor, + ln, + log2, + divides, + minus, + mod, + multiplies, + plus, + sin, + sqrt, + tan +}; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index d01b9720..0303aee4 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -17,6 +17,20 @@ struct DynamicOperator { using Type = dynamic::Operation::Abs; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Ceil; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Cos; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -24,6 +38,34 @@ struct DynamicOperator { using Type = dynamic::Operation::Divides; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Exp; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Floor; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Ln; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Log2; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -52,6 +94,27 @@ struct DynamicOperator { using Type = dynamic::Operation::Plus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Sin; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Sqrt; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::numerical; + using Type = dynamic::Operation::Tan; +}; + template using dynamic_operator_t = typename DynamicOperator::Type; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index da1ee981..b3da9b99 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -367,13 +367,31 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("ceil({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("cos({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("({}) / ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("exp({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("floor({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("ln({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("log(2.0, {})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); @@ -390,6 +408,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("sin({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("sqrt({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index a1604128..9031b456 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -327,13 +327,31 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("ceil({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("cos({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("({}) / ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("exp({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("floor({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("ln({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("log2({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); @@ -350,6 +368,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("sin({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("sqrt({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index 8a16811c..ffcb96a2 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -34,14 +34,17 @@ TEST(sqlite, test_operations) { int id_plus_2_minus_age; int age_mod_3; int abs_age; + int exp_age; + int sqrt_age; }; const auto get_children = - select_from(("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - ("age"_c % 3) | as<"age_mod_3">, - abs("age"_c * (-1)) | as<"abs_age">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + select_from( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, + abs("age"_c * (-1)) | as<"abs_age">, exp("age"_c) | as<"exp_age">, + sqrt("age"_c) | as<"sqrt_age">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -51,7 +54,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026,"sqrt_age":3},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980,"sqrt_age":2},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1,"sqrt_age":0}])"; EXPECT_EQ(rfl::json::write(children), expected); } From fb06c1520e8feca52af8fd629d6416fb2a3dc57e Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Wed, 18 Jun 2025 23:59:51 +0200 Subject: [PATCH 12/25] Added cast operations --- include/sqlgen/dynamic/Operation.hpp | 12 +++++++--- include/sqlgen/operations.hpp | 10 +++++++++ include/sqlgen/transpilation/Operation.hpp | 4 ++++ include/sqlgen/transpilation/Operator.hpp | 3 ++- .../sqlgen/transpilation/OperatorCategory.hpp | 2 +- .../transpilation/dynamic_operator_t.hpp | 7 ++++++ include/sqlgen/transpilation/make_field.hpp | 22 +++++++++++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 6 +++++ src/sqlgen/postgres/to_sql.cpp | 4 ++++ src/sqlgen/sqlite/to_sql.cpp | 4 ++++ tests/postgres/test_operations.cpp | 6 ++++- tests/sqlite/test_operations.cpp | 19 ++++++++-------- 12 files changed, 84 insertions(+), 15 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 49bc6799..294eb548 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -6,6 +6,7 @@ #include "../Ref.hpp" #include "Aggregation.hpp" #include "Column.hpp" +#include "Type.hpp" #include "Value.hpp" namespace sqlgen::dynamic { @@ -15,6 +16,11 @@ struct Operation { Ref op1; }; + struct Cast { + Ref op1; + Type target_type; + }; + struct Ceil { Ref op1; }; @@ -77,9 +83,9 @@ struct Operation { }; using ReflectionType = - rfl::TaggedUnion<"what", Abs, Aggregation, Ceil, Column, Cos, Divides, - Exp, Floor, Ln, Log2, Minus, Mod, Multiplies, Plus, Sin, - Sqrt, Tan, Value>; + rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Cos, + Divides, Exp, Floor, Ln, Log2, Minus, Mod, Multiplies, + Plus, Sin, Sqrt, Tan, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index f64d3660..36bfc622 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -19,6 +19,16 @@ auto abs(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto cast(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation< + transpilation::Operator::cast, Type, + transpilation::TypeHolder>>{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto ceil(const T& _t) { using Type = diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 05ad05b1..469c9a37 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -12,6 +12,10 @@ namespace sqlgen::transpilation { +/// Simple abstraction to be used for the cast operation. +template +struct TypeHolder {}; + template struct Operation { static constexpr Operator op = _op; diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 7e397350..56432ecb 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -5,13 +5,14 @@ namespace sqlgen::transpilation { enum class Operator { abs, + cast, ceil, cos, + divides, exp, floor, ln, log2, - divides, minus, mod, multiplies, diff --git a/include/sqlgen/transpilation/OperatorCategory.hpp b/include/sqlgen/transpilation/OperatorCategory.hpp index 0311c0ca..3092d82b 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 }; +enum class OperatorCategory { 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 0303aee4..09fe81aa 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -17,6 +17,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Abs; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Cast; +}; + 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 9b0db774..c2227740 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -7,6 +7,7 @@ #include "../Literal.hpp" #include "../Result.hpp" #include "../dynamic/SelectFrom.hpp" +#include "../parsing/Parser.hpp" #include "Aggregation.hpp" #include "AggregationOp.hpp" #include "As.hpp" @@ -158,6 +159,27 @@ struct MakeField> { } }; +template +struct MakeField>> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = std::remove_cvref_t; + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::Cast{ + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .target_type = + parsing::Parser>::to_type()}}}; + } +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 4271e3fb..5b50dd8b 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -30,6 +30,12 @@ struct Underlying>> { using Type = remove_reflection_t>; }; +template +struct Underlying< + T, Operation>> { + using Type = std::remove_cvref_t; +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index b3da9b99..75d393c2 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -367,6 +367,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("CAST({} AS {})", operation_to_sql(*_s.op1), + type_to_sql(_s.target_type)); + } else if constexpr (std::is_same_v) { return std::format("ceil({})", operation_to_sql(*_s.op1)); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 9031b456..01574541 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -327,6 +327,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return aggregation_to_sql(_s); + } else if constexpr (std::is_same_v) { + return std::format("CAST({} AS {})", operation_to_sql(*_s.op1), + type_to_sql(_s.target_type)); + } else if constexpr (std::is_same_v) { return std::format("ceil({})", operation_to_sql(*_s.op1)); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index 5528b3fa..b2f1faad 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -40,6 +40,8 @@ TEST(postgres, test_operations) { int id_plus_2_minus_age; int age_mod_3; int abs_age; + double exp_age; + double sqrt_age; }; const auto get_children = @@ -47,6 +49,8 @@ TEST(postgres, test_operations) { ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, abs("age"_c * (-1)) | as<"abs_age">, + exp(cast("age"_c)) | as<"exp_age">, + sqrt(cast("age"_c)) | as<"sqrt_age">, ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -58,7 +62,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.465794806718,"sqrt_age":3.1622776601683795},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.9579870417283,"sqrt_age":2.8284271247461903},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index ffcb96a2..1256bf02 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -34,17 +34,18 @@ TEST(sqlite, test_operations) { int id_plus_2_minus_age; int age_mod_3; int abs_age; - int exp_age; - int sqrt_age; + double exp_age; + double sqrt_age; }; const auto get_children = - select_from( - ("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, - abs("age"_c * (-1)) | as<"abs_age">, exp("age"_c) | as<"exp_age">, - sqrt("age"_c) | as<"sqrt_age">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + select_from(("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("age"_c % 3) | as<"age_mod_3">, + abs("age"_c * (-1)) | as<"abs_age">, + exp(cast("age"_c)) | as<"exp_age">, + sqrt(cast("age"_c)) | as<"sqrt_age">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -54,7 +55,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026,"sqrt_age":3},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980,"sqrt_age":2},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1,"sqrt_age":0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.4657948067,"sqrt_age":3.16227766016838},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.95798704173,"sqrt_age":2.82842712474619},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 471a0de2dd3379576ad3d1cc35dd65104ac58a49 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 00:00:01 +0200 Subject: [PATCH 13/25] Added more compelling examples to the README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 7155573f..7655f261 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,17 @@ const auto query = read> | const auto query = read> | where("age"_c == "Homer"); +// Compile-time error: "age" must be aggregated or included in GROUP BY +const auto query = select_from( + "last_name"_c, + "age"_c +) | group_by("last_name"_c); + +// Compile-time error: Cannot add string and int +const auto query = select_from( + "last_name"_c + "age"_c +); + // Runtime protection against SQL injection std::vector get_people(const auto& conn, const sqlgen::AlphaNumeric& first_name) { From 64b904f2796204e2f500f97f8a37e6274b0b3db3 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 11:53:12 +0200 Subject: [PATCH 14/25] Added the round(...) operation --- include/sqlgen/dynamic/Operation.hpp | 7 +++++- include/sqlgen/operations.hpp | 11 ++++++++ include/sqlgen/transpilation/Operator.hpp | 1 + .../transpilation/dynamic_operator_t.hpp | 7 ++++++ include/sqlgen/transpilation/make_field.hpp | 25 +++++++++++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 15 +++++++++++ src/sqlgen/postgres/to_sql.cpp | 11 +++++--- src/sqlgen/sqlite/to_sql.cpp | 4 +++ tests/postgres/test_operations.cpp | 16 ++++++------ tests/sqlite/test_operations.cpp | 16 ++++++------ 10 files changed, 92 insertions(+), 21 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 294eb548..66488872 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -70,6 +70,11 @@ struct Operation { Ref op2; }; + struct Round { + Ref op1; + Ref op2; + }; + struct Sin { Ref op1; }; @@ -85,7 +90,7 @@ struct Operation { using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Cos, Divides, Exp, Floor, Ln, Log2, Minus, Mod, Multiplies, - Plus, Sin, Sqrt, Tan, Value>; + Plus, Round, Sin, Sqrt, Tan, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index 36bfc622..e704e50e 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -77,6 +77,17 @@ auto log2(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto round(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 sin(const T& _t) { using Type = diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 56432ecb..58dcc74e 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -17,6 +17,7 @@ enum class Operator { mod, multiplies, plus, + round, sin, sqrt, tan diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 09fe81aa..bd20b8e2 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -101,6 +101,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Plus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Round; +}; + 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 c2227740..55f36025 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -180,6 +180,31 @@ struct MakeField +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = + underlying_t>; + + 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::numerical) diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 5b50dd8b..c0d50dfc 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -36,6 +36,21 @@ struct Underlying< using Type = std::remove_cvref_t; }; +template +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + using Underlying2 = + typename Underlying>::Type; + + static_assert(std::is_integral_v> || + std::is_floating_point_v>, + "Must be a numerical type"); + static_assert(std::is_integral_v, "Must be an integral type"); + + using Type = Underlying1; +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 75d393c2..680b879a 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -412,6 +412,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("round({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("sin({})", operation_to_sql(*_s.op1)); @@ -527,10 +531,9 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept { } else if constexpr (std::is_same_v || std::is_same_v) { return "BIGINT"; - } else if constexpr (std::is_same_v) { - return "REAL"; - } else if constexpr (std::is_same_v) { - return "DOUBLE PRECISION"; + } else if constexpr (std::is_same_v || + std::is_same_v) { + return "NUMERIC"; } else if constexpr (std::is_same_v) { return "TEXT"; } else if constexpr (std::is_same_v) { diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 01574541..e8d45c67 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -372,6 +372,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("round({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("sin({})", operation_to_sql(*_s.op1)); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index b2f1faad..d36127ec 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -45,13 +45,13 @@ TEST(postgres, test_operations) { }; const auto get_children = - select_from(("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - ("age"_c % 3) | as<"age_mod_3">, - abs("age"_c * (-1)) | as<"abs_age">, - exp(cast("age"_c)) | as<"exp_age">, - sqrt(cast("age"_c)) | as<"sqrt_age">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + select_from( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, + 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">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -62,7 +62,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.465794806718,"sqrt_age":3.1622776601683795},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.9579870417283,"sqrt_age":2.8284271247461903},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index 1256bf02..bfc0fd36 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -39,13 +39,13 @@ TEST(sqlite, test_operations) { }; const auto get_children = - select_from(("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - ("age"_c % 3) | as<"age_mod_3">, - abs("age"_c * (-1)) | as<"abs_age">, - exp(cast("age"_c)) | as<"exp_age">, - sqrt(cast("age"_c)) | as<"sqrt_age">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + select_from( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, ("age"_c % 3) | as<"age_mod_3">, + 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">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -55,7 +55,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.4657948067,"sqrt_age":3.16227766016838},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.95798704173,"sqrt_age":2.82842712474619},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 3dd630e8c2700915c474d9f9a33cba450d36b33b Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 16:17:36 +0200 Subject: [PATCH 15/25] Added string-based operators --- include/sqlgen/dynamic/Operation.hpp | 39 +++++++++- include/sqlgen/operations.hpp | 68 ++++++++++++++++++ include/sqlgen/transpilation/Operator.hpp | 9 ++- .../transpilation/dynamic_operator_t.hpp | 51 +++++++++++++ include/sqlgen/transpilation/make_field.hpp | 71 +++++++++++++++++++ .../transpilation/to_transpilation_type.hpp | 7 ++ include/sqlgen/transpilation/underlying_t.hpp | 55 ++++++++++++++ src/sqlgen/postgres/to_sql.cpp | 27 +++++++ src/sqlgen/sqlite/to_sql.cpp | 27 +++++++ tests/postgres/test_operations.cpp | 8 ++- tests/sqlite/test_operations.cpp | 8 ++- 11 files changed, 362 insertions(+), 8 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 66488872..d184106d 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -2,6 +2,7 @@ #define SQLGEN_DYNAMIC_OPERATION_HPP_ #include +#include #include "../Ref.hpp" #include "Aggregation.hpp" @@ -25,6 +26,10 @@ struct Operation { Ref op1; }; + struct Concat { + std::vector> ops; + }; + struct Cos { Ref op1; }; @@ -42,10 +47,23 @@ struct Operation { Ref op1; }; + struct Length { + Ref op1; + }; + struct Ln { Ref op1; }; + struct Lower { + Ref op1; + }; + + struct LTrim { + Ref op1; + Ref op2; + }; + struct Log2 { Ref op1; }; @@ -75,6 +93,11 @@ struct Operation { Ref op2; }; + struct RTrim { + Ref op1; + Ref op2; + }; + struct Sin { Ref op1; }; @@ -87,10 +110,20 @@ struct Operation { Ref op1; }; + struct Trim { + Ref op1; + Ref op2; + }; + + struct Upper { + Ref op1; + }; + using ReflectionType = - rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Cos, - Divides, Exp, Floor, Ln, Log2, Minus, Mod, Multiplies, - Plus, Round, Sin, Sqrt, Tan, Value>; + rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Concat, + Cos, Divides, Exp, Floor, Length, Ln, Log2, Lower, LTrim, + Minus, Mod, Multiplies, Plus, Round, RTrim, Sin, Sqrt, + Tan, Upper, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index e704e50e..bf5333d3 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -37,6 +37,17 @@ auto ceil(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto concat(const T& _t, const Ts&... _ts) { + using Type = rfl::Tuple< + typename transpilation::ToTranspilationType>::Type, + typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type...>; + return transpilation::Operation{ + .operand1 = Type(transpilation::to_transpilation_type(_t), + transpilation::to_transpilation_type(_ts)...)}; +} + template auto cos(const T& _t) { using Type = @@ -61,6 +72,14 @@ auto floor(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto length(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto ln(const T& _t) { using Type = @@ -77,6 +96,25 @@ auto log2(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto lower(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto ltrim(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 round(const T& _t, const U& _u) { using Type1 = @@ -88,6 +126,17 @@ auto round(const T& _t, const U& _u) { .operand2 = transpilation::to_transpilation_type(_u)}; } +template +auto rtrim(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 sin(const T& _t) { using Type = @@ -112,6 +161,25 @@ auto tan(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto trim(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 upper(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 58dcc74e..765fc204 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -7,20 +7,27 @@ enum class Operator { abs, cast, ceil, + concat, cos, divides, exp, floor, + length, ln, log2, + lower, + ltrim, minus, mod, multiplies, plus, round, + rtrim, sin, sqrt, - tan + tan, + trim, + upper }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index bd20b8e2..ebb57dbb 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -1,6 +1,8 @@ #ifndef SQLGEN_TRANSPILATION_DYNAMICOPERATORT_HPP_ #define SQLGEN_TRANSPILATION_DYNAMICOPERATORT_HPP_ +#include + #include "../dynamic/Operation.hpp" #include "Operator.hpp" #include "OperatorCategory.hpp" @@ -31,6 +33,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Ceil; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = std::numeric_limits::max(); + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::Concat; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -59,6 +68,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Floor; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::Length; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -73,6 +89,20 @@ struct DynamicOperator { using Type = dynamic::Operation::Log2; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::Lower; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::LTrim; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -108,6 +138,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Round; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::RTrim; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -129,6 +166,20 @@ struct DynamicOperator { using Type = dynamic::Operation::Tan; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::Trim; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::string; + using Type = dynamic::Operation::Upper; +}; + template using dynamic_operator_t = typename DynamicOperator::Type; diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 55f36025..4d042c61 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -180,6 +180,32 @@ struct MakeField +struct MakeField>> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = + underlying_t>>; + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::Concat{ + .ops = rfl::apply( + [](const auto&... _ops) { + return std::vector>( + {Ref::make( + MakeField>{}(_ops) + .val)...}); + }, + _o.operand1)}}}; + } +}; + template struct MakeField> { @@ -205,6 +231,51 @@ struct MakeField + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::string) +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = underlying_t>; + + 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)}}}; + } +}; + +template + requires((num_operands_v<_op>) == 2 && + (operator_category_v<_op>) == OperatorCategory::string) +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = underlying_t>; + + 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)}}}; + } +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/include/sqlgen/transpilation/to_transpilation_type.hpp b/include/sqlgen/transpilation/to_transpilation_type.hpp index 99e3021d..eecf39ab 100644 --- a/include/sqlgen/transpilation/to_transpilation_type.hpp +++ b/include/sqlgen/transpilation/to_transpilation_type.hpp @@ -27,6 +27,13 @@ struct ToTranspilationType { Type operator()(const char* _val) const noexcept { return make_value(_val); } }; +template +struct ToTranspilationType { + using Type = Value; + + Type operator()(const char* _val) const noexcept { return make_value(_val); } +}; + template struct ToTranspilationType> { using Type = Aggregation<_agg, _ValueType>; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index c0d50dfc..210bff2a 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -2,6 +2,7 @@ #define SQLGEN_TRANSPILATION_UNDERLYINGT_HPP_ #include +#include #include #include "Col.hpp" @@ -51,6 +52,60 @@ struct Underlying> { using Type = Underlying1; }; +template +struct Underlying>> { + static_assert( + (true && ... && + std::is_same_v>::Type>, + std::string>), + "Must be a string"); + + using Type = + std::conditional_t<(false || ... || + is_nullable_v>::Type>), + std::optional, std::string>; +}; + +template + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::string) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + + static_assert(std::is_same_v, std::string>, + "Must be a string"); + + using StringType = + std::conditional_t, std::optional, + std::string>; + using SizeType = std::conditional_t, + std::optional, size_t>; + using Type = + std::conditional_t<_op == Operator::length, SizeType, StringType>; +}; + +template + requires((num_operands_v<_op>) == 2 && + (operator_category_v<_op>) == OperatorCategory::string) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + using Underlying2 = + typename Underlying>::Type; + + static_assert(std::is_same_v, std::string>, + "Must be a string"); + static_assert(std::is_same_v, std::string>, + "Must be a string"); + + using Type = std::conditional_t || + is_nullable_v, + std::optional, std::string>; +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::numerical) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 680b879a..393860ef 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -359,6 +359,7 @@ std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept { } std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { + using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { @@ -377,6 +378,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + return "(" + + internal::strings::join( + " || ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + + ")"; + } else if constexpr (std::is_same_v) { return std::format("cos({})", operation_to_sql(*_s.op1)); @@ -390,12 +400,22 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("floor({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("length({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("ln({})", operation_to_sql(*_s.op1)); } else if constexpr (std::is_same_v) { return std::format("log(2.0, {})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("lower({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("ltrim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); @@ -416,6 +436,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("round({}, {})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("rtrim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("sin({})", operation_to_sql(*_s.op1)); @@ -425,6 +449,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("upper({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index e8d45c67..0d4017c6 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -319,6 +319,7 @@ std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept { } std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { + using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { @@ -337,6 +338,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + return "(" + + internal::strings::join( + " || ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + + ")"; + } else if constexpr (std::is_same_v) { return std::format("cos({})", operation_to_sql(*_s.op1)); @@ -350,12 +360,22 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("floor({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("length({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return std::format("ln({})", operation_to_sql(*_s.op1)); } else if constexpr (std::is_same_v) { return std::format("log2({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("lower({})", operation_to_sql(*_s.op1)); + + } else if constexpr (std::is_same_v) { + return std::format("ltrim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("({}) - ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); @@ -376,6 +396,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("round({}, {})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("rtrim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("sin({})", operation_to_sql(*_s.op1)); @@ -385,6 +409,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("upper({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index d36127ec..a4bf1422 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -42,6 +42,8 @@ TEST(postgres, test_operations) { int abs_age; double exp_age; double sqrt_age; + size_t length_first_name; + std::string full_name; }; const auto get_children = @@ -51,7 +53,9 @@ TEST(postgres, test_operations) { 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">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + length("first_name"_c) | as<"length_first_name">, + concat("first_name"_c, " ", "last_name"_c) | as<"full_name">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -62,7 +66,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index bfc0fd36..5bcf1036 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -36,6 +36,8 @@ TEST(sqlite, test_operations) { int abs_age; double exp_age; double sqrt_age; + size_t length_first_name; + std::string full_name; }; const auto get_children = @@ -45,7 +47,9 @@ TEST(sqlite, test_operations) { 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">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + length("first_name"_c) | as<"length_first_name">, + concat("first_name"_c, " ", "last_name"_c) | as<"full_name">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -55,7 +59,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson"}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 6e37904a3b05e1643db41a3af28ec68d9bdf46fe Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 17:00:35 +0200 Subject: [PATCH 16/25] Added more tests --- include/sqlgen/dynamic/Operation.hpp | 2 +- include/sqlgen/operations.hpp | 15 +++++++++++++++ include/sqlgen/transpilation/make_field.hpp | 3 ++- src/sqlgen/postgres/to_sql.cpp | 4 ++++ src/sqlgen/sqlite/to_sql.cpp | 4 ++++ tests/postgres/test_operations.cpp | 11 ++++++++--- tests/postgres/test_operations_with_nullable.cpp | 9 ++++++--- tests/sqlite/test_operations.cpp | 11 ++++++++--- tests/sqlite/test_operations_with_nullable.cpp | 9 ++++++--- 9 files changed, 54 insertions(+), 14 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index d184106d..f175d966 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -123,7 +123,7 @@ struct Operation { rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Concat, Cos, Divides, Exp, Floor, Length, Ln, Log2, Lower, LTrim, Minus, Mod, Multiplies, Plus, Round, RTrim, Sin, Sqrt, - Tan, Upper, Value>; + Tan, Trim, Upper, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index bf5333d3..a913f129 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -115,6 +115,11 @@ auto ltrim(const T& _t, const U& _u) { .operand2 = transpilation::to_transpilation_type(_u)}; } +template +auto ltrim(const T& _t) { + return ltrim(_t, std::string(" ")); +} + template auto round(const T& _t, const U& _u) { using Type1 = @@ -137,6 +142,11 @@ auto rtrim(const T& _t, const U& _u) { .operand2 = transpilation::to_transpilation_type(_u)}; } +template +auto rtrim(const T& _t) { + return rtrim(_t, std::string(" ")); +} + template auto sin(const T& _t) { using Type = @@ -172,6 +182,11 @@ auto trim(const T& _t, const U& _u) { .operand2 = transpilation::to_transpilation_type(_u)}; } +template +auto trim(const T& _t) { + return trim(_t, std::string(" ")); +} + template auto upper(const T& _t) { using Type = diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 4d042c61..840912ef 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -260,7 +260,8 @@ struct MakeField> { static constexpr bool is_column = false; using Name = Nothing; - using Type = underlying_t>; + using Type = + underlying_t>; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 393860ef..6bd955a5 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -449,6 +449,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("trim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("upper({})", operation_to_sql(*_s.op1)); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 0d4017c6..0b65736d 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -409,6 +409,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return std::format("tan({})", operation_to_sql(*_s.op1)); + } else if constexpr (std::is_same_v) { + return std::format("trim({}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { return std::format("upper({})", operation_to_sql(*_s.op1)); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index a4bf1422..21da7ad8 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -44,6 +44,8 @@ TEST(postgres, test_operations) { double sqrt_age; size_t length_first_name; std::string full_name; + std::string first_name_lower; + std::string first_name_upper; }; const auto get_children = @@ -54,8 +56,11 @@ TEST(postgres, test_operations) { round(exp(cast("age"_c)), 2) | as<"exp_age">, round(sqrt(cast("age"_c)), 2) | as<"sqrt_age">, ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, - length("first_name"_c) | as<"length_first_name">, - concat("first_name"_c, " ", "last_name"_c) | as<"full_name">) | + length(trim("first_name"_c)) | as<"length_first_name">, + concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | + as<"full_name">, + upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, + lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -66,7 +71,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/postgres/test_operations_with_nullable.cpp b/tests/postgres/test_operations_with_nullable.cpp index 7ea50995..bcdc62af 100644 --- a/tests/postgres/test_operations_with_nullable.cpp +++ b/tests/postgres/test_operations_with_nullable.cpp @@ -14,7 +14,7 @@ namespace test_operations_with_nullable { struct Person { sqlgen::PrimaryKey id; std::string first_name; - std::string last_name; + std::optional last_name; std::optional age; }; @@ -38,12 +38,15 @@ TEST(postgres, test_operations_with_nullable) { std::optional id_plus_age; std::optional age_times_2; std::optional id_plus_2_minus_age; + std::optional full_name; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">) | + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + concat(upper("last_name"_c), ", ", "first_name"_c) | + as<"full_name">) | where("age"_c < 18) | to>; const auto children = postgres::connect(credentials) @@ -53,7 +56,7 @@ TEST(postgres, test_operations_with_nullable) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"full_name":"SIMPSON, Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"full_name":"SIMPSON, Maggie"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index 5bcf1036..36bc6201 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -38,6 +38,8 @@ TEST(sqlite, test_operations) { double sqrt_age; size_t length_first_name; std::string full_name; + std::string first_name_lower; + std::string first_name_upper; }; const auto get_children = @@ -48,8 +50,11 @@ TEST(sqlite, test_operations) { round(exp(cast("age"_c)), 2) | as<"exp_age">, round(sqrt(cast("age"_c)), 2) | as<"sqrt_age">, ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, - length("first_name"_c) | as<"length_first_name">, - concat("first_name"_c, " ", "last_name"_c) | as<"full_name">) | + length(trim("first_name"_c)) | as<"length_first_name">, + concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | + as<"full_name">, + upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, + lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -59,7 +64,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations_with_nullable.cpp b/tests/sqlite/test_operations_with_nullable.cpp index b3267826..ba7143d9 100644 --- a/tests/sqlite/test_operations_with_nullable.cpp +++ b/tests/sqlite/test_operations_with_nullable.cpp @@ -32,13 +32,16 @@ TEST(sqlite, test_operations_with_nullable) { struct Children { std::optional id_plus_age; std::optional age_times_2; - std::optional age_plus_2_minus_id; + std::optional id_plus_2_minus_age; + std::optional full_name; }; const auto get_children = select_from(("id"_c + "age"_c) | as<"id_plus_age">, ("age"_c * 2) | as<"age_times_2">, - ("age"_c + 2 - "id"_c) | as<"age_plus_2_minus_id">) | + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + concat(upper("last_name"_c), ", ", "first_name"_c) | + as<"full_name">) | where("age"_c < 18) | to>; const auto children = sqlite::connect() @@ -47,7 +50,7 @@ TEST(sqlite, test_operations_with_nullable) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"age_plus_2_minus_id":11},{"id_plus_age":10,"age_times_2":16,"age_plus_2_minus_id":8},{"id_plus_age":3,"age_times_2":0,"age_plus_2_minus_id":-1}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"full_name":"SIMPSON, Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"full_name":"SIMPSON, Maggie"}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 08f8c41f669ffdcddcc73c19f6a6dded10fd6715 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 19:25:03 +0200 Subject: [PATCH 17/25] Added the string replace operator --- include/sqlgen/dynamic/Operation.hpp | 10 +++- include/sqlgen/operations.hpp | 15 ++++++ include/sqlgen/transpilation/Operation.hpp | 5 +- include/sqlgen/transpilation/Operator.hpp | 1 + .../transpilation/dynamic_operator_t.hpp | 7 +++ include/sqlgen/transpilation/make_field.hpp | 30 +++++++++++ include/sqlgen/transpilation/underlying_t.hpp | 53 +++++++++++++------ src/sqlgen/postgres/to_sql.cpp | 4 ++ src/sqlgen/sqlite/to_sql.cpp | 4 ++ tests/postgres/test_operations.cpp | 6 ++- tests/sqlite/test_operations.cpp | 6 ++- 11 files changed, 119 insertions(+), 22 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index f175d966..95a05419 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -88,6 +88,12 @@ struct Operation { Ref op2; }; + struct Replace { + Ref op1; + Ref op2; + Ref op3; + }; + struct Round { Ref op1; Ref op2; @@ -122,8 +128,8 @@ struct Operation { using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Concat, Cos, Divides, Exp, Floor, Length, Ln, Log2, Lower, LTrim, - Minus, Mod, Multiplies, Plus, Round, RTrim, Sin, Sqrt, - Tan, Trim, Upper, Value>; + Minus, Mod, Multiplies, Plus, Replace, Round, RTrim, Sin, + Sqrt, Tan, Trim, Upper, Value>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index a913f129..4b29bde5 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -120,6 +120,21 @@ auto ltrim(const T& _t) { return ltrim(_t, std::string(" ")); } +template +auto replace(const StringType& _str, const FromType& _from, const ToType& _to) { + using Type1 = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + using Type2 = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + using Type3 = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_str), + .operand2 = transpilation::to_transpilation_type(_from), + .operand3 = transpilation::to_transpilation_type(_to)}; +} + template auto round(const T& _t, const U& _u) { using Type1 = diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 469c9a37..133f1359 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -16,15 +16,18 @@ namespace sqlgen::transpilation { template struct TypeHolder {}; -template +template struct Operation { static constexpr Operator op = _op; using Operand1Type = _Operand1Type; using Operand2Type = _Operand2Type; + using Operand3Type = _Operand3Type; Operand1Type operand1; Operand2Type operand2; + Operand3Type operand3; /// Returns an IS NULL condition. auto is_null() const noexcept { diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index 765fc204..ad0c316c 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -21,6 +21,7 @@ enum class Operator { mod, multiplies, plus, + replace, round, rtrim, sin, diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index ebb57dbb..85cc7b3a 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -131,6 +131,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Plus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 3; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Replace; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 840912ef..6604a3ed 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -206,6 +206,36 @@ struct MakeField +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + + using Name = Nothing; + using Type = + underlying_t>; + + 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> { diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 210bff2a..3fb1660a 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -37,21 +37,6 @@ struct Underlying< using Type = std::remove_cvref_t; }; -template -struct Underlying> { - using Underlying1 = - typename Underlying>::Type; - using Underlying2 = - typename Underlying>::Type; - - static_assert(std::is_integral_v> || - std::is_floating_point_v>, - "Must be a numerical type"); - static_assert(std::is_integral_v, "Must be an integral type"); - - using Type = Underlying1; -}; - template struct Underlying>> { static_assert( @@ -68,6 +53,44 @@ struct Underlying>> { std::optional, std::string>; }; +template +struct Underlying< + T, Operation> { + using Underlying1 = + typename Underlying>::Type; + using Underlying2 = + typename Underlying>::Type; + using Underlying3 = + typename Underlying>::Type; + + static_assert(std::is_same_v, std::string>, + "Must be a string"); + static_assert(std::is_same_v, std::string>, + "Must be a string"); + static_assert(std::is_same_v, std::string>, + "Must be a string"); + + using Type = std::conditional_t || + is_nullable_v || + is_nullable_v, + std::optional, std::string>; +}; + +template +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + using Underlying2 = + typename Underlying>::Type; + + static_assert(std::is_integral_v> || + std::is_floating_point_v>, + "Must be a numerical type"); + static_assert(std::is_integral_v, "Must be an integral type"); + + using Type = Underlying1; +}; + 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 6bd955a5..1966f666 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -432,6 +432,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("replace({}, {}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2), operation_to_sql(*_s.op3)); + } else if constexpr (std::is_same_v) { return std::format("round({}, {})", 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 0b65736d..2e6b94a4 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -392,6 +392,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return std::format("({}) + ({})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); + } else if constexpr (std::is_same_v) { + return std::format("replace({}, {}, {})", operation_to_sql(*_s.op1), + operation_to_sql(*_s.op2), operation_to_sql(*_s.op3)); + } else if constexpr (std::is_same_v) { return std::format("round({}, {})", operation_to_sql(*_s.op1), operation_to_sql(*_s.op2)); diff --git a/tests/postgres/test_operations.cpp b/tests/postgres/test_operations.cpp index 21da7ad8..635ddc41 100644 --- a/tests/postgres/test_operations.cpp +++ b/tests/postgres/test_operations.cpp @@ -46,6 +46,7 @@ TEST(postgres, test_operations) { std::string full_name; std::string first_name_lower; std::string first_name_upper; + std::string first_name_replaced; }; const auto get_children = @@ -60,7 +61,8 @@ TEST(postgres, test_operations) { concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | as<"full_name">, upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, - lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">) | + lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">, + replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -71,7 +73,7 @@ TEST(postgres, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART","first_name_replaced":"Hugo"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA","first_name_replaced":"Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE","first_name_replaced":"Maggie"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations.cpp b/tests/sqlite/test_operations.cpp index 36bc6201..1a370611 100644 --- a/tests/sqlite/test_operations.cpp +++ b/tests/sqlite/test_operations.cpp @@ -40,6 +40,7 @@ TEST(sqlite, test_operations) { std::string full_name; std::string first_name_lower; std::string first_name_upper; + std::string first_name_replaced; }; const auto get_children = @@ -54,7 +55,8 @@ TEST(sqlite, test_operations) { concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | as<"full_name">, upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, - lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">) | + lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">, + replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced">) | where("age"_c < 18) | order_by("age"_c.desc()) | to>; @@ -64,7 +66,7 @@ TEST(sqlite, test_operations) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"age_mod_3":1,"abs_age":10,"exp_age":22026.47,"sqrt_age":3.16,"length_first_name":4,"full_name":"Bart Simpson","first_name_lower":"bart","first_name_upper":"BART","first_name_replaced":"Hugo"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"age_mod_3":2,"abs_age":8,"exp_age":2980.96,"sqrt_age":2.83,"length_first_name":4,"full_name":"Lisa Simpson","first_name_lower":"lisa","first_name_upper":"LISA","first_name_replaced":"Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"age_mod_3":0,"abs_age":0,"exp_age":1.0,"sqrt_age":0.0,"length_first_name":6,"full_name":"Maggie Simpson","first_name_lower":"maggie","first_name_upper":"MAGGIE","first_name_replaced":"Maggie"}])"; EXPECT_EQ(rfl::json::write(children), expected); } From 11b2ce8e55b17f0735adae1866edf4187ef675dc Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 20:23:02 +0200 Subject: [PATCH 18/25] Added coalesce(...) --- include/sqlgen/dynamic/Operation.hpp | 12 ++++--- include/sqlgen/operations.hpp | 25 ++++++++++----- include/sqlgen/transpilation/Operator.hpp | 1 + .../transpilation/dynamic_operator_t.hpp | 7 +++++ include/sqlgen/transpilation/make_field.hpp | 31 +++++++++---------- include/sqlgen/transpilation/underlying_t.hpp | 17 ++++++++++ src/sqlgen/postgres/to_sql.cpp | 11 ++++++- src/sqlgen/sqlite/to_sql.cpp | 11 ++++++- .../test_operations_with_nullable.cpp | 19 +++++++----- .../sqlite/test_operations_with_nullable.cpp | 21 +++++++------ 10 files changed, 108 insertions(+), 47 deletions(-) diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 95a05419..08ffc098 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -26,6 +26,10 @@ struct Operation { Ref op1; }; + struct Coalesce { + std::vector> ops; + }; + struct Concat { std::vector> ops; }; @@ -126,10 +130,10 @@ struct Operation { }; using ReflectionType = - rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Concat, - Cos, Divides, Exp, Floor, Length, Ln, Log2, Lower, LTrim, - Minus, Mod, Multiplies, Plus, Replace, Round, RTrim, Sin, - Sqrt, Tan, Trim, Upper, Value>; + 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>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index 4b29bde5..8db48fd4 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -37,15 +37,24 @@ auto ceil(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } -template -auto concat(const T& _t, const Ts&... _ts) { - using Type = rfl::Tuple< - typename transpilation::ToTranspilationType>::Type, - typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type...>; +template +auto coalesce(const Ts&... _ts) { + static_assert(sizeof...(_ts) > 1, + "coalesce(...) must have at least two inputs."); + using Type = rfl::Tuple>::Type...>; + return transpilation::Operation{ + .operand1 = Type(transpilation::to_transpilation_type(_ts)...)}; +} + +template +auto concat(const Ts&... _ts) { + static_assert(sizeof...(_ts) > 0, + "concat(...) must have at least one input."); + using Type = rfl::Tuple>::Type...>; return transpilation::Operation{ - .operand1 = Type(transpilation::to_transpilation_type(_t), - transpilation::to_transpilation_type(_ts)...)}; + .operand1 = Type(transpilation::to_transpilation_type(_ts)...)}; } template diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index ad0c316c..b6a037ed 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -7,6 +7,7 @@ enum class Operator { abs, cast, ceil, + coalesce, concat, cos, divides, diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 85cc7b3a..95c696ee 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -33,6 +33,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Ceil; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = std::numeric_limits::max(); + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Coalesce; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = std::numeric_limits::max(); diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 6604a3ed..103ae5a1 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -180,29 +180,28 @@ struct MakeField -struct MakeField>> { +template + requires((_op == Operator::coalesce) || (_op == Operator::concat)) +struct MakeField>> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; using Name = Nothing; using Type = - underlying_t>>; + underlying_t>>; dynamic::SelectFrom::Field operator()(const auto& _o) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{dynamic::Operation::Concat{ - .ops = rfl::apply( - [](const auto&... _ops) { - return std::vector>( - {Ref::make( - MakeField>{}(_ops) - .val)...}); - }, - _o.operand1)}}}; + 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)}}}; } }; diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 3fb1660a..b1b897da 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -37,6 +37,23 @@ struct Underlying< using Type = std::remove_cvref_t; }; +template +struct Underlying>> { + using Operand1Type = typename Underlying>::Type; + + static_assert((true && ... && + std::is_same_v, + remove_nullable_t>::Type>>), + "All inputs into coalesce(...) must have the same type."); + + using Type = std::conditional_t< + (is_nullable_v && ... && + is_nullable_v>::Type>), + std::optional>, + remove_nullable_t>; +}; + template struct Underlying>> { static_assert( diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 1966f666..f89dd708 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -369,7 +369,7 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { - return std::format("CAST({} AS {})", operation_to_sql(*_s.op1), + return std::format("cast({} as {})", operation_to_sql(*_s.op1), type_to_sql(_s.target_type)); } else if constexpr (std::is_same_v) { @@ -378,6 +378,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { return column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + return "coalesce(" + + internal::strings::join( + ", ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + + ")"; + } else if constexpr (std::is_same_v) { return "(" + internal::strings::join( diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 2e6b94a4..bb0170f0 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -329,9 +329,18 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { return aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { - return std::format("CAST({} AS {})", operation_to_sql(*_s.op1), + return std::format("cast({} as {})", operation_to_sql(*_s.op1), type_to_sql(_s.target_type)); + } else if constexpr (std::is_same_v) { + return "coalesce(" + + internal::strings::join( + ", ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + + ")"; + } else if constexpr (std::is_same_v) { return std::format("ceil({})", operation_to_sql(*_s.op1)); diff --git a/tests/postgres/test_operations_with_nullable.cpp b/tests/postgres/test_operations_with_nullable.cpp index bcdc62af..5aedfdd1 100644 --- a/tests/postgres/test_operations_with_nullable.cpp +++ b/tests/postgres/test_operations_with_nullable.cpp @@ -23,9 +23,10 @@ TEST(postgres, test_operations_with_nullable) { {Person{ .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, - Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{.id = 2, .first_name = "Hugo", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, Person{ - .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + .id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", .password = "password", @@ -39,14 +40,16 @@ TEST(postgres, test_operations_with_nullable) { std::optional age_times_2; std::optional id_plus_2_minus_age; std::optional full_name; + std::string last_name_or_none; }; const auto get_children = - select_from(("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, - concat(upper("last_name"_c), ", ", "first_name"_c) | - as<"full_name">) | + select_from( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name">, + coalesce("last_name"_c, "none") | as<"last_name_or_none">) | where("age"_c < 18) | to>; const auto children = postgres::connect(credentials) @@ -56,7 +59,7 @@ TEST(postgres, test_operations_with_nullable) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"full_name":"SIMPSON, Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"full_name":"SIMPSON, Maggie"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart","last_name_or_none":"Simpson"},{"id_plus_age":12,"age_times_2":20,"id_plus_2_minus_age":-6,"last_name_or_none":"none"},{"id_plus_age":11,"age_times_2":16,"id_plus_2_minus_age":-3,"full_name":"SIMPSON, Lisa","last_name_or_none":"Simpson"},{"id_plus_age":4,"age_times_2":0,"id_plus_2_minus_age":6,"full_name":"SIMPSON, Maggie","last_name_or_none":"Simpson"}])"; EXPECT_EQ(rfl::json::write(children), expected); } diff --git a/tests/sqlite/test_operations_with_nullable.cpp b/tests/sqlite/test_operations_with_nullable.cpp index ba7143d9..df80928b 100644 --- a/tests/sqlite/test_operations_with_nullable.cpp +++ b/tests/sqlite/test_operations_with_nullable.cpp @@ -14,7 +14,7 @@ namespace test_operations_with_nullable { struct Person { sqlgen::PrimaryKey id; std::string first_name; - std::string last_name; + std::optional last_name; std::optional age; }; @@ -23,9 +23,10 @@ TEST(sqlite, test_operations_with_nullable) { {Person{ .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, - Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{.id = 2, .first_name = "Hugo", .age = 10}, + Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, Person{ - .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + .id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); using namespace sqlgen; @@ -34,14 +35,16 @@ TEST(sqlite, test_operations_with_nullable) { std::optional age_times_2; std::optional id_plus_2_minus_age; std::optional full_name; + std::string last_name_or_none; }; const auto get_children = - select_from(("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, - concat(upper("last_name"_c), ", ", "first_name"_c) | - as<"full_name">) | + select_from( + ("id"_c + "age"_c) | as<"id_plus_age">, + ("age"_c * 2) | as<"age_times_2">, + ("id"_c + 2 - "age"_c) | as<"id_plus_2_minus_age">, + concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name">, + coalesce(upper("last_name"_c), "none") | as<"last_name_or_none">) | where("age"_c < 18) | to>; const auto children = sqlite::connect() @@ -50,7 +53,7 @@ TEST(sqlite, test_operations_with_nullable) { .value(); const std::string expected = - R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart"},{"id_plus_age":10,"age_times_2":16,"id_plus_2_minus_age":-4,"full_name":"SIMPSON, Lisa"},{"id_plus_age":3,"age_times_2":0,"id_plus_2_minus_age":5,"full_name":"SIMPSON, Maggie"}])"; + R"([{"id_plus_age":11,"age_times_2":20,"id_plus_2_minus_age":-7,"full_name":"SIMPSON, Bart","last_name_or_none":"SIMPSON"},{"id_plus_age":12,"age_times_2":20,"id_plus_2_minus_age":-6,"last_name_or_none":"none"},{"id_plus_age":11,"age_times_2":16,"id_plus_2_minus_age":-3,"full_name":"SIMPSON, Lisa","last_name_or_none":"SIMPSON"},{"id_plus_age":4,"age_times_2":0,"id_plus_2_minus_age":6,"full_name":"SIMPSON, Maggie","last_name_or_none":"SIMPSON"}])"; EXPECT_EQ(rfl::json::write(children), expected); } From d4f60799983bd8e90f5ea2c57db5a64d46ccc936 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 20:33:01 +0200 Subject: [PATCH 19/25] Make sure that cast respects NULL --- include/sqlgen/transpilation/underlying_t.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index b1b897da..3076b94d 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -34,7 +34,11 @@ struct Underlying>> { template struct Underlying< T, Operation>> { - using Type = std::remove_cvref_t; + using Type = + std::conditional_t>::Type>, + std::optional>, + std::remove_cvref_t>; }; template From 1dda22e77f15d0f0743a46fcc6f285fcdd4d90d8 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 19 Jun 2025 21:04:58 +0200 Subject: [PATCH 20/25] Added documentation for other operations and functions --- docs/README.md | 1 + docs/other_operations.md | 233 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 docs/other_operations.md diff --git a/docs/README.md b/docs/README.md index 93a90811..de41ca5c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::group_by and Aggregations](group_by_and_aggregations.md) - How generate GROUP BY queries and aggregate data - [sqlgen::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 ## Data Types and Validation diff --git a/docs/other_operations.md b/docs/other_operations.md new file mode 100644 index 00000000..b033d6ca --- /dev/null +++ b/docs/other_operations.md @@ -0,0 +1,233 @@ +# 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(ltrim("first_name"_c), " ", rtrim("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 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 + +// All columns nullable: result is nullable +coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> // Result is std::optional +``` + +### 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 (or vice versa). +- 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(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | as<"full_name">, + lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">, + upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, + replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> +) | where("age"_c < 18) | to>; +``` + +--- + +## 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. + From 0622e486c37e14d7591f9fb3ed5bbf0e14643ad1 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 20 Jun 2025 22:08:21 +0200 Subject: [PATCH 21/25] Removed any references to std::format --- src/sqlgen/postgres/to_sql.cpp | 174 +++++++++++++++++---------------- src/sqlgen/sqlite/to_sql.cpp | 170 +++++++++++++++++--------------- 2 files changed, 180 insertions(+), 164 deletions(-) diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index f89dd708..403ab183 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -1,6 +1,5 @@ #include "sqlgen/postgres/to_sql.hpp" -#include #include #include #include @@ -74,28 +73,29 @@ std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { using Type = std::remove_cvref_t; - + std::stringstream stream; if constexpr (std::is_same_v) { - return std::format("AVG({})", column_or_value_to_sql(_agg.val)); + stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { const auto val = std::string(_agg.val && _agg.distinct ? "DISTINCT " : "") + (_agg.val ? column_or_value_to_sql(*_agg.val) : std::string("*")); - return std::format("COUNT({})", val); + stream << "COUNT(" << val << ")"; } else if constexpr (std::is_same_v) { - return std::format("MAX({})", column_or_value_to_sql(_agg.val)); + stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { - return std::format("MIN({})", column_or_value_to_sql(_agg.val)); + stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { - return std::format("SUM({})", column_or_value_to_sql(_agg.val)); + stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } + return stream.str(); }); } @@ -129,55 +129,58 @@ template std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { using C = std::remove_cvref_t; + std::stringstream stream; + if constexpr (std::is_same_v) { - return std::format("({}) AND ({})", condition_to_sql(*_condition.cond1), - condition_to_sql(*_condition.cond2)); + stream << "(" << condition_to_sql(*_condition.cond1) << ") AND (" + << condition_to_sql(*_condition.cond2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("{} = {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " = " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} >= {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " >= " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} > {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " > " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} IS NULL", operation_to_sql(_condition.op)); + stream << operation_to_sql(_condition.op) << " IS NULL"; } else if constexpr (std::is_same_v) { - return std::format("{} IS NOT NULL", operation_to_sql(_condition.op)); + stream << operation_to_sql(_condition.op) << " IS NOT NULL"; } else if constexpr (std::is_same_v) { - return std::format("{} <= {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " <= " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} < {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " < " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} LIKE {}", operation_to_sql(_condition.op), - column_or_value_to_sql(_condition.pattern)); + stream << operation_to_sql(_condition.op) << " LIKE " + << column_or_value_to_sql(_condition.pattern); } else if constexpr (std::is_same_v) { - return std::format("{} != {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " != " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} NOT LIKE {}", operation_to_sql(_condition.op), - column_or_value_to_sql(_condition.pattern)); + stream << operation_to_sql(_condition.op) << " NOT LIKE " + << column_or_value_to_sql(_condition.pattern); } else if constexpr (std::is_same_v) { - return std::format("({}) OR ({})", condition_to_sql(*_condition.cond1), - condition_to_sql(*_condition.cond2)); + stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" + << condition_to_sql(*_condition.cond2) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } + return stream.str(); } std::string column_to_sql_definition(const dynamic::Column& _col) noexcept { @@ -362,119 +365,124 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; + + std::stringstream stream; + if constexpr (std::is_same_v) { - return std::format("abs({})", operation_to_sql(*_s.op1)); + stream << "abs(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return aggregation_to_sql(_s); + stream << aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { - return std::format("cast({} as {})", operation_to_sql(*_s.op1), - type_to_sql(_s.target_type)); + stream << "cast(" << operation_to_sql(*_s.op1) << " as " + << type_to_sql(_s.target_type) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "coalesce(" + << internal::strings::join( + ", ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("ceil({})", operation_to_sql(*_s.op1)); + stream << "ceil(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return column_or_value_to_sql(_s); - - } else if constexpr (std::is_same_v) { - return "coalesce(" + - internal::strings::join( - ", ", internal::collect::vector( - _s.ops | transform([](const auto& _op) { - return operation_to_sql(*_op); - }))) + - ")"; + stream << column_or_value_to_sql(_s); } else if constexpr (std::is_same_v) { - return "(" + - internal::strings::join( - " || ", internal::collect::vector( - _s.ops | transform([](const auto& _op) { - return operation_to_sql(*_op); - }))) + - ")"; + stream << "(" + << internal::strings::join( + " || ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("cos({})", operation_to_sql(*_s.op1)); + stream << "cos(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) / ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") / (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("exp({})", operation_to_sql(*_s.op1)); + stream << "exp(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("floor({})", operation_to_sql(*_s.op1)); + stream << "floor(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("length({})", operation_to_sql(*_s.op1)); + stream << "length(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("ln({})", operation_to_sql(*_s.op1)); + stream << "ln(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("log(2.0, {})", operation_to_sql(*_s.op1)); + stream << "log(2.0, " << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("lower({})", operation_to_sql(*_s.op1)); + stream << "lower(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("ltrim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "ltrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) - ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") - (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("mod({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "mod(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) * ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") * (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) + ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") + (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("replace({}, {}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2), operation_to_sql(*_s.op3)); + stream << "replace(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ", " << operation_to_sql(*_s.op3) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("round({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "round(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("rtrim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("sin({})", operation_to_sql(*_s.op1)); + stream << "sin(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("sqrt({})", operation_to_sql(*_s.op1)); + stream << "sqrt(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("tan({})", operation_to_sql(*_s.op1)); + stream << "tan(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("trim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "trim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("upper({})", operation_to_sql(*_s.op1)); + stream << "upper(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return column_or_value_to_sql(_s); + stream << column_or_value_to_sql(_s); } else { static_assert(rfl::always_false_v, "Unsupported type."); } + return stream.str(); }); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index bb0170f0..81b2650d 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -52,28 +51,29 @@ std::string aggregation_to_sql( const dynamic::Aggregation& _aggregation) noexcept { return _aggregation.val.visit([](const auto& _agg) -> std::string { using Type = std::remove_cvref_t; - + std::stringstream stream; if constexpr (std::is_same_v) { - return std::format("AVG({})", column_or_value_to_sql(_agg.val)); + stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { const auto val = std::string(_agg.val && _agg.distinct ? "DISTINCT " : "") + (_agg.val ? column_or_value_to_sql(*_agg.val) : std::string("*")); - return std::format("COUNT({})", val); + stream << "COUNT(" << val << ")"; } else if constexpr (std::is_same_v) { - return std::format("MAX({})", column_or_value_to_sql(_agg.val)); + stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { - return std::format("MIN({})", column_or_value_to_sql(_agg.val)); + stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; } else if constexpr (std::is_same_v) { - return std::format("SUM({})", column_or_value_to_sql(_agg.val)); + stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } + return stream.str(); }); } @@ -113,55 +113,58 @@ template std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { using C = std::remove_cvref_t; + std::stringstream stream; + if constexpr (std::is_same_v) { - return std::format("({}) AND ({})", condition_to_sql(*_condition.cond1), - condition_to_sql(*_condition.cond2)); + stream << "(" << condition_to_sql(*_condition.cond1) << ") AND (" + << condition_to_sql(*_condition.cond2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("{} = {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " = " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} >= {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " >= " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} > {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " > " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} IS NULL", operation_to_sql(_condition.op)); + stream << operation_to_sql(_condition.op) << " IS NULL"; } else if constexpr (std::is_same_v) { - return std::format("{} IS NOT NULL", operation_to_sql(_condition.op)); + stream << operation_to_sql(_condition.op) << " IS NOT NULL"; } else if constexpr (std::is_same_v) { - return std::format("{} <= {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " <= " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} < {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) << " < " + << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} LIKE {}", operation_to_sql(_condition.op), - column_or_value_to_sql(_condition.pattern)); + stream << operation_to_sql(_condition.op) << " LIKE " + << column_or_value_to_sql(_condition.pattern); } else if constexpr (std::is_same_v) { - return std::format("{} != {}", operation_to_sql(_condition.op1), - operation_to_sql(_condition.op2)); + stream << operation_to_sql(_condition.op1) + << " != " << operation_to_sql(_condition.op2); } else if constexpr (std::is_same_v) { - return std::format("{} NOT LIKE {}", operation_to_sql(_condition.op), - column_or_value_to_sql(_condition.pattern)); + stream << operation_to_sql(_condition.op) << " NOT LIKE " + << column_or_value_to_sql(_condition.pattern); } else if constexpr (std::is_same_v) { - return std::format("({}) OR ({})", condition_to_sql(*_condition.cond1), - condition_to_sql(*_condition.cond2)); + stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" + << condition_to_sql(*_condition.cond2) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); } + return stream.str(); } std::string create_index_to_sql(const dynamic::CreateIndex& _stmt) noexcept { @@ -322,119 +325,124 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { using namespace std::ranges::views; return _stmt.val.visit([](const auto& _s) -> std::string { using Type = std::remove_cvref_t; + + std::stringstream stream; + if constexpr (std::is_same_v) { - return std::format("abs({})", operation_to_sql(*_s.op1)); + stream << "abs(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return aggregation_to_sql(_s); + stream << aggregation_to_sql(_s); } else if constexpr (std::is_same_v) { - return std::format("cast({} as {})", operation_to_sql(*_s.op1), - type_to_sql(_s.target_type)); + stream << "cast(" << operation_to_sql(*_s.op1) << " as " + << type_to_sql(_s.target_type) << ")"; } else if constexpr (std::is_same_v) { - return "coalesce(" + - internal::strings::join( - ", ", internal::collect::vector( - _s.ops | transform([](const auto& _op) { - return operation_to_sql(*_op); - }))) + - ")"; + stream << "coalesce(" + << internal::strings::join( + ", ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("ceil({})", operation_to_sql(*_s.op1)); + stream << "ceil(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return column_or_value_to_sql(_s); + stream << column_or_value_to_sql(_s); } else if constexpr (std::is_same_v) { - return "(" + - internal::strings::join( - " || ", internal::collect::vector( - _s.ops | transform([](const auto& _op) { - return operation_to_sql(*_op); - }))) + - ")"; + stream << "(" + << internal::strings::join( + " || ", internal::collect::vector( + _s.ops | transform([](const auto& _op) { + return operation_to_sql(*_op); + }))) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("cos({})", operation_to_sql(*_s.op1)); + stream << "cos(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) / ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") / (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("exp({})", operation_to_sql(*_s.op1)); + stream << "exp(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("floor({})", operation_to_sql(*_s.op1)); + stream << "floor(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("length({})", operation_to_sql(*_s.op1)); + stream << "length(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("ln({})", operation_to_sql(*_s.op1)); + stream << "ln(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("log2({})", operation_to_sql(*_s.op1)); + stream << "log2(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("lower({})", operation_to_sql(*_s.op1)); + stream << "lower(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("ltrim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "ltrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) - ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") - (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("mod({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "mod(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) * ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") * (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("({}) + ({})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "(" << operation_to_sql(*_s.op1) << ") + (" + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("replace({}, {}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2), operation_to_sql(*_s.op3)); + stream << "replace(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ", " << operation_to_sql(*_s.op3) + << ")"; } else if constexpr (std::is_same_v) { - return std::format("round({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "round(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("rtrim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("sin({})", operation_to_sql(*_s.op1)); + stream << "sin(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("sqrt({})", operation_to_sql(*_s.op1)); + stream << "sqrt(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("tan({})", operation_to_sql(*_s.op1)); + stream << "tan(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return std::format("trim({}, {})", operation_to_sql(*_s.op1), - operation_to_sql(*_s.op2)); + stream << "trim(" << operation_to_sql(*_s.op1) << ", " + << operation_to_sql(*_s.op2) << ")"; } else if constexpr (std::is_same_v) { - return std::format("upper({})", operation_to_sql(*_s.op1)); + stream << "upper(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { - return column_or_value_to_sql(_s); + stream << column_or_value_to_sql(_s); } else { static_assert(rfl::always_false_v, "Unsupported type."); } + return stream.str(); }); } From b4457852d5fa411ca326bb8c117efc6274964485 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 20 Jun 2025 22:08:37 +0200 Subject: [PATCH 22/25] Some improvements to the documentation --- docs/other_operations.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/other_operations.md b/docs/other_operations.md index b033d6ca..5b993cbf 100644 --- a/docs/other_operations.md +++ b/docs/other_operations.md @@ -93,7 +93,7 @@ replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> Concatenates multiple strings or expressions. ```cpp -concat(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | as<"full_name"> +concat("first_name"_c, " ", "last_name"_c) | as<"full_name"> concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name"> ``` @@ -135,7 +135,7 @@ When using these operations on nullable columns (e.g., `std::optional`), the - **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 operations** (e.g., `+`, `concat`, `replace`, etc.): +- **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`)**: @@ -178,15 +178,13 @@ coalesce(std::optional{}, "default") // -> std::string ```cpp // Provide a default for a nullable column coalesce("last_name"_c, "none") | as<"last_name_or_none"> // Result is std::string - -// All columns nullable: result is nullable -coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> // Result is std::optional +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 (or vice versa). +- 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. @@ -216,13 +214,31 @@ const auto get_children = select_from( 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(ltrim("first_name"_c), " ", rtrim("last_name"_c)) | as<"full_name">, - lower(ltrim(concat(" ", "first_name"_c))) | as<"first_name_lower">, - upper(rtrim(concat("first_name"_c, " "))) | as<"first_name_upper">, + 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 From 28ef8e5e5cf04332e504e8b3f34ff1db09b142b8 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 20 Jun 2025 22:36:08 +0200 Subject: [PATCH 23/25] Added operator NOT --- include/sqlgen/dynamic/Condition.hpp | 6 +++- include/sqlgen/transpilation/Condition.hpp | 28 +++++++++++-------- include/sqlgen/transpilation/conditions.hpp | 7 +++++ include/sqlgen/transpilation/to_condition.hpp | 10 +++++++ src/sqlgen/postgres/to_sql.cpp | 3 ++ src/sqlgen/sqlite/to_sql.cpp | 3 ++ tests/postgres/test_where.cpp | 2 +- tests/sqlite/test_where.cpp | 2 +- 8 files changed, 46 insertions(+), 15 deletions(-) diff --git a/include/sqlgen/dynamic/Condition.hpp b/include/sqlgen/dynamic/Condition.hpp index 3cea3b52..75ac4d07 100644 --- a/include/sqlgen/dynamic/Condition.hpp +++ b/include/sqlgen/dynamic/Condition.hpp @@ -54,6 +54,10 @@ struct Condition { dynamic::Value pattern; }; + struct Not { + Ref cond; + }; + struct NotEqual { Operation op1; Operation op2; @@ -71,7 +75,7 @@ struct Condition { using ReflectionType = rfl::TaggedUnion<"what", And, Equal, GreaterEqual, GreaterThan, IsNull, - IsNotNull, LesserEqual, LesserThan, Like, NotEqual, + IsNotNull, LesserEqual, LesserThan, Like, Not, NotEqual, NotLike, Or>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/transpilation/Condition.hpp b/include/sqlgen/transpilation/Condition.hpp index 1b864173..a8db48e5 100644 --- a/include/sqlgen/transpilation/Condition.hpp +++ b/include/sqlgen/transpilation/Condition.hpp @@ -11,6 +11,22 @@ template struct Condition { using ConditionType = _ConditionType; ConditionType condition; + + auto operator!() { + return make_condition(conditions::Not<_ConditionType>{.cond = condition}); + } + + template + friend auto operator&&(const Condition& _cond1, const Condition& _cond2) { + return make_condition(conditions::And<_ConditionType, C2>{ + .cond1 = _cond1.condition, .cond2 = _cond2.condition}); + } + + template + friend auto operator||(const Condition& _cond1, const Condition& _cond2) { + return make_condition(conditions::Or<_ConditionType, C2>{ + .cond1 = _cond1.condition, .cond2 = _cond2.condition}); + } }; template @@ -18,18 +34,6 @@ auto make_condition(T&& _t) { return Condition>{.condition = _t}; } -template -auto operator&&(const Condition& _cond1, const Condition& _cond2) { - return make_condition(conditions::And{.cond1 = _cond1.condition, - .cond2 = _cond2.condition}); -} - -template -auto operator||(const Condition& _cond1, const Condition& _cond2) { - return make_condition(conditions::Or{.cond1 = _cond1.condition, - .cond2 = _cond2.condition}); -} - } // namespace sqlgen::transpilation #endif diff --git a/include/sqlgen/transpilation/conditions.hpp b/include/sqlgen/transpilation/conditions.hpp index ac3af5aa..f9eb4383 100644 --- a/include/sqlgen/transpilation/conditions.hpp +++ b/include/sqlgen/transpilation/conditions.hpp @@ -126,6 +126,13 @@ struct NotEqual { OpType2 op2; }; +template +struct Not { + using ResultType = bool; + + CondType cond; +}; + template auto not_equal(const OpType1& _op1, const OpType2& _op2) { return NotEqual, std::remove_cvref_t>{ diff --git a/include/sqlgen/transpilation/to_condition.hpp b/include/sqlgen/transpilation/to_condition.hpp index 3d452c21..113b77c5 100644 --- a/include/sqlgen/transpilation/to_condition.hpp +++ b/include/sqlgen/transpilation/to_condition.hpp @@ -136,6 +136,16 @@ struct ToCondition> { } }; +template +struct ToCondition> { + dynamic::Condition operator()(const auto& _cond) const { + return dynamic::Condition{ + .val = dynamic::Condition::Not{ + .cond = Ref::make( + ToCondition>{}(_cond.cond))}}; + } +}; + template struct ToCondition> { static_assert(std::equality_comparable_with, diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 403ab183..983d2433 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -165,6 +165,9 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << operation_to_sql(_condition.op) << " LIKE " << column_or_value_to_sql(_condition.pattern); + } else if constexpr (std::is_same_v) { + stream << "NOT (" << condition_to_sql(*_condition.cond) << ")"; + } else if constexpr (std::is_same_v) { stream << operation_to_sql(_condition.op1) << " != " << operation_to_sql(_condition.op2); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 81b2650d..739aa01a 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -149,6 +149,9 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << operation_to_sql(_condition.op) << " LIKE " << column_or_value_to_sql(_condition.pattern); + } else if constexpr (std::is_same_v) { + stream << "NOT (" << condition_to_sql(*_condition.cond) << ")"; + } else if constexpr (std::is_same_v) { stream << operation_to_sql(_condition.op1) << " != " << operation_to_sql(_condition.op2); diff --git a/tests/postgres/test_where.cpp b/tests/postgres/test_where.cpp index ddaed0ae..4b001359 100644 --- a/tests/postgres/test_where.cpp +++ b/tests/postgres/test_where.cpp @@ -41,7 +41,7 @@ TEST(postgres, test_where) { sqlgen::write(conn, people1).value(); const auto query = sqlgen::read> | - where("age"_c < 18 and "first_name"_c != "Hugo") | + where("age"_c < 18 and not("first_name"_c == "Hugo")) | order_by("age"_c); const auto people2 = query(conn).value(); diff --git a/tests/sqlite/test_where.cpp b/tests/sqlite/test_where.cpp index 57dbc1a6..8baef743 100644 --- a/tests/sqlite/test_where.cpp +++ b/tests/sqlite/test_where.cpp @@ -33,7 +33,7 @@ TEST(sqlite, test_where) { using namespace sqlgen; const auto query = sqlgen::read> | - where("age"_c < 18 and "first_name"_c != "Hugo") | + where("age"_c < 18 and not("first_name"_c == "Hugo")) | order_by("age"_c); const auto people2 = query(conn).value(); From 861cf14ce0b55de2d7ac860e209e4f4239df50c5 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 22 Jun 2025 14:19:14 +0200 Subject: [PATCH 24/25] Added GROUP BY checks for the operations --- include/sqlgen/operations.hpp | 5 ++ .../transpilation/check_aggregations.hpp | 3 +- .../sqlgen/transpilation/flatten_fields_t.hpp | 64 ++++++++++++++++++ include/sqlgen/transpilation/make_field.hpp | 24 +++++++ .../sqlgen/transpilation/to_select_from.hpp | 9 ++- .../test_group_by_with_operations.cpp | 66 +++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 include/sqlgen/transpilation/flatten_fields_t.hpp create mode 100644 tests/postgres/test_group_by_with_operations.cpp diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index 8db48fd4..ca8c01a5 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -155,6 +155,11 @@ auto round(const T& _t, const U& _u) { .operand2 = transpilation::to_transpilation_type(_u)}; } +template +auto round(const T& _t) { + return round(_t, 0); +} + template auto rtrim(const T& _t, const U& _u) { using Type1 = diff --git a/include/sqlgen/transpilation/check_aggregations.hpp b/include/sqlgen/transpilation/check_aggregations.hpp index 85b7498b..05c70865 100644 --- a/include/sqlgen/transpilation/check_aggregations.hpp +++ b/include/sqlgen/transpilation/check_aggregations.hpp @@ -49,7 +49,8 @@ struct CheckAggregation, static constexpr bool value = (true && ... && (MakeField::is_aggregation || - !MakeField::is_column || + (!MakeField::is_column && + !MakeField::is_operation) || included_in_group_by)); static_assert(value, diff --git a/include/sqlgen/transpilation/flatten_fields_t.hpp b/include/sqlgen/transpilation/flatten_fields_t.hpp new file mode 100644 index 00000000..441e3bab --- /dev/null +++ b/include/sqlgen/transpilation/flatten_fields_t.hpp @@ -0,0 +1,64 @@ +#ifndef SQLGEN_TRANSPILATION_FLATTEN_FIELDS_T_HPP_ +#define SQLGEN_TRANSPILATION_FLATTEN_FIELDS_T_HPP_ + +#include +#include + +#include "As.hpp" +#include "make_field.hpp" +#include "remove_as_t.hpp" + +namespace sqlgen::transpilation { + +template +struct TupleWrapper; + +template +struct TupleWrapper> { + using Type = rfl::Tuple; + + template + friend consteval auto operator+( + const TupleWrapper&, + const TupleWrapper>&) { + return TupleWrapper>{}; + } +}; + +template +struct FlattenFields; + +template +struct ExtractFields; + +template + requires(!MakeField>::is_operation) +struct ExtractFields { + using Type = rfl::Tuple>; +}; + +template + requires(MakeField>::is_operation) +struct ExtractFields { + using Type = typename FlattenFields< + StructType, + typename MakeField>::Operands>::Type; +}; + +template +struct FlattenFields> { + static constexpr auto wrapper = + (TupleWrapper>{} + ... + + TupleWrapper::Type>{}); + + using Type = typename decltype(wrapper)::Type; +}; + +template +using flatten_fields_t = + typename FlattenFields, + std::remove_cvref_t>::Type; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 103ae5a1..9add2276 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -32,6 +32,7 @@ template struct MakeField { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; + static constexpr bool is_operation = false; using Name = Nothing; using Type = std::remove_cvref_t; @@ -49,6 +50,7 @@ struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = true; + static constexpr bool is_operation = false; using Name = Literal<_name>; using Type = rfl::field_type_t<_name, StructType>; @@ -63,6 +65,7 @@ template struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; + static constexpr bool is_operation = false; using Name = Nothing; using Type = std::remove_cvref_t; @@ -79,6 +82,8 @@ struct MakeField> { static constexpr bool is_aggregation = MakeField::is_aggregation; static constexpr bool is_column = MakeField::is_column; + static constexpr bool is_operation = + MakeField::is_operation; using Name = Literal<_new_name>; using Type = @@ -110,6 +115,7 @@ struct MakeField>> { static constexpr bool is_aggregation = true; static constexpr bool is_column = true; + static constexpr bool is_operation = false; using Name = Literal<_name>; using Type = rfl::field_type_t<_name, StructType>; @@ -130,6 +136,7 @@ struct MakeField>> { static constexpr bool is_aggregation = true; static constexpr bool is_column = true; + static constexpr bool is_operation = false; using Name = Literal<_name>; using Type = size_t; @@ -147,6 +154,7 @@ template struct MakeField> { static constexpr bool is_aggregation = true; static constexpr bool is_column = true; + static constexpr bool is_operation = false; using Name = Nothing; using Type = size_t; @@ -164,9 +172,11 @@ 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 = std::remove_cvref_t; + using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { return dynamic::SelectFrom::Field{ @@ -185,10 +195,12 @@ 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 { using DynamicOperatorType = dynamic_operator_t<_op>; @@ -211,11 +223,13 @@ 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{ @@ -240,11 +254,13 @@ 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{ @@ -266,9 +282,11 @@ 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 { using DynamicOperatorType = dynamic_operator_t<_op>; @@ -287,10 +305,12 @@ template > { 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>; @@ -312,9 +332,11 @@ 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 { using DynamicOperatorType = dynamic_operator_t<_op>; @@ -333,10 +355,12 @@ template > { 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>; diff --git a/include/sqlgen/transpilation/to_select_from.hpp b/include/sqlgen/transpilation/to_select_from.hpp index b237f276..b0bf6b2c 100644 --- a/include/sqlgen/transpilation/to_select_from.hpp +++ b/include/sqlgen/transpilation/to_select_from.hpp @@ -14,6 +14,7 @@ #include "../dynamic/Table.hpp" #include "../internal/collect/vector.hpp" #include "check_aggregations.hpp" +#include "flatten_fields_t.hpp" #include "get_schema.hpp" #include "get_tablename.hpp" #include "make_fields.hpp" @@ -31,9 +32,11 @@ template (), - "The aggregations were not set up correctly. Please check the " - "trace for a more detailed error message."); + static_assert( + check_aggregations, + GroupByType>(), + "The aggregations were not set up correctly. Please check the " + "trace for a more detailed error message."); const auto fields = make_fields( _fields, diff --git a/tests/postgres/test_group_by_with_operations.cpp b/tests/postgres/test_group_by_with_operations.cpp new file mode 100644 index 00000000..43925c36 --- /dev/null +++ b/tests/postgres/test_group_by_with_operations.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_group_by_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_group_by) { + static_assert(std::ranges::input_range>, + "Must be an input range."); + + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + struct Children { + std::string last_name; + std::string last_name_trimmed; + double avg_age; + }; + + const auto get_children = + select_from("last_name"_c, + trim("last_name"_c) | as<"last_name_trimmed">, + avg("age"_c) | as<"avg_age">) | + where("age"_c < 18) | group_by("last_name"_c) | to>; + + const auto children = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + EXPECT_EQ(children.size(), 1); + EXPECT_EQ(children.at(0).last_name, "Simpson"); + EXPECT_EQ(children.at(0).last_name_trimmed, "Simpson"); + EXPECT_EQ(children.at(0).avg_age, 6.0); +} + +} // namespace test_group_by_with_operations + +#endif From 24b621743faff3f1b2981ea157ec7c51147db03c Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Sun, 22 Jun 2025 15:52:45 +0200 Subject: [PATCH 25/25] Make sure we can use operations inside of aggregations and vice versa --- include/sqlgen/aggregations.hpp | 45 ++++++++------ include/sqlgen/dynamic/Aggregation.hpp | 37 +---------- include/sqlgen/dynamic/Operation.hpp | 32 +++++++++- include/sqlgen/transpilation/Aggregation.hpp | 55 ++++++++++++++++ include/sqlgen/transpilation/Operation.hpp | 7 +++ include/sqlgen/transpilation/make_field.hpp | 38 ++++++------ .../transpilation/to_transpilation_type.hpp | 9 --- include/sqlgen/transpilation/underlying_t.hpp | 7 +++ src/sqlgen/postgres/to_sql.cpp | 8 +-- src/sqlgen/sqlite/to_sql.cpp | 8 +-- .../test_group_by_with_operations.cpp | 12 +++- .../sqlite/test_group_by_with_operations.cpp | 62 +++++++++++++++++++ 12 files changed, 224 insertions(+), 96 deletions(-) create mode 100644 tests/sqlite/test_group_by_with_operations.cpp diff --git a/include/sqlgen/aggregations.hpp b/include/sqlgen/aggregations.hpp index 17e50c83..37e33f63 100644 --- a/include/sqlgen/aggregations.hpp +++ b/include/sqlgen/aggregations.hpp @@ -7,14 +7,16 @@ #include "col.hpp" #include "transpilation/Aggregation.hpp" #include "transpilation/AggregationOp.hpp" +#include "transpilation/to_transpilation_type.hpp" namespace sqlgen { -template -auto avg(const Col<_name>&) { - return transpilation::Aggregation>{ - .val = transpilation::Col<_name>{}}; +template +auto avg(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Aggregation{ + .val = transpilation::to_transpilation_type(_t)}; } inline auto count() { @@ -36,25 +38,28 @@ auto count_distinct(const Col<_name>&) { .val = transpilation::Col<_name>{}, .distinct = true}; } -template -auto max(const Col<_name>&) { - return transpilation::Aggregation>{ - .val = transpilation::Col<_name>{}}; +template +auto max(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Aggregation{ + .val = transpilation::to_transpilation_type(_t)}; } -template -auto min(const Col<_name>&) { - return transpilation::Aggregation>{ - .val = transpilation::Col<_name>{}}; +template +auto min(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Aggregation{ + .val = transpilation::to_transpilation_type(_t)}; } -template -auto sum(const Col<_name>&) { - return transpilation::Aggregation>{ - .val = transpilation::Col<_name>{}}; +template +auto sum(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Aggregation{ + .val = transpilation::to_transpilation_type(_t)}; } } // namespace sqlgen diff --git a/include/sqlgen/dynamic/Aggregation.hpp b/include/sqlgen/dynamic/Aggregation.hpp index 3537fd85..e98c8154 100644 --- a/include/sqlgen/dynamic/Aggregation.hpp +++ b/include/sqlgen/dynamic/Aggregation.hpp @@ -1,44 +1,11 @@ #ifndef SQLGEN_DYNAMIC_AGGREGATION_HPP_ #define SQLGEN_DYNAMIC_AGGREGATION_HPP_ -#include -#include -#include -#include - -#include "Column.hpp" -#include "ColumnOrValue.hpp" +#include "Operation.hpp" namespace sqlgen::dynamic { -struct Aggregation { - struct Avg { - ColumnOrValue val; - }; - - struct Count { - std::optional val; - bool distinct = false; - }; - - struct Max { - ColumnOrValue val; - }; - - struct Min { - ColumnOrValue val; - }; - - struct Sum { - ColumnOrValue val; - }; - - using ReflectionType = rfl::TaggedUnion<"what", Avg, Count, Max, Min, Sum>; - - const ReflectionType& reflection() const { return val; } - - ReflectionType val; -}; +using Aggregation = Operation::Aggregation; } // namespace sqlgen::dynamic diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 08ffc098..7360521e 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -1,11 +1,12 @@ #ifndef SQLGEN_DYNAMIC_OPERATION_HPP_ #define SQLGEN_DYNAMIC_OPERATION_HPP_ +#include #include +#include #include #include "../Ref.hpp" -#include "Aggregation.hpp" #include "Column.hpp" #include "Type.hpp" #include "Value.hpp" @@ -17,6 +18,35 @@ struct Operation { Ref op1; }; + struct Aggregation { + struct Avg { + Ref val; + }; + + struct Count { + std::optional val; + bool distinct = false; + }; + + struct Max { + Ref val; + }; + + struct Min { + Ref val; + }; + + struct Sum { + Ref val; + }; + + using ReflectionType = rfl::TaggedUnion<"what", Avg, Count, Max, Min, Sum>; + + const ReflectionType& reflection() const { return val; } + + ReflectionType val; + }; + struct Cast { Ref op1; Type target_type; diff --git a/include/sqlgen/transpilation/Aggregation.hpp b/include/sqlgen/transpilation/Aggregation.hpp index 8e8033ea..058eda38 100644 --- a/include/sqlgen/transpilation/Aggregation.hpp +++ b/include/sqlgen/transpilation/Aggregation.hpp @@ -5,6 +5,9 @@ #include "AggregationOp.hpp" #include "As.hpp" +#include "Operation.hpp" +#include "Operator.hpp" +#include "to_transpilation_type.hpp" namespace sqlgen::transpilation { @@ -24,6 +27,58 @@ struct Aggregation { ValueType val; bool distinct = false; + + template + friend auto operator/(const Aggregation& _op1, const T& _op2) noexcept { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } + + template + friend auto operator-(const Aggregation& _op1, const T& _op2) noexcept { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } + + template + friend auto operator%(const Aggregation& _op1, const T& _op2) noexcept { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } + + template + friend auto operator*(const Aggregation& _op1, const T& _op2) noexcept { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } + + template + friend auto operator+(const Aggregation& _op1, const T& _op2) noexcept { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } +}; + +template +struct ToTranspilationType> { + using Type = Aggregation<_agg, _ValueType>; + + Type operator()(const Type& _val) const noexcept { return _val; } }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 133f1359..887b9e9c 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -5,6 +5,7 @@ #include #include "../Result.hpp" +#include "As.hpp" #include "Condition.hpp" #include "Operator.hpp" #include "conditions.hpp" @@ -29,6 +30,12 @@ struct Operation { Operand2Type operand2; Operand3Type operand3; + template + auto as() const noexcept { + using T = std::remove_cvref_t; + return transpilation::As{.val = *this}; + } + /// Returns an IS NULL condition. auto is_null() const noexcept { return make_condition(conditions::is_null(*this)); diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index 9add2276..5ebeb934 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -100,31 +100,30 @@ struct MakeField> { } }; -template -struct MakeField>> { - static_assert(all_columns_exist>(), - "A column required in the aggregation does not exist."); - - static_assert( - std::is_integral_v< - remove_nullable_t>>> || - std::is_floating_point_v< - remove_nullable_t>>>, - "Values inside the aggregation must be numerical."); +template +struct MakeField> { + static_assert(std::is_integral_v< + remove_nullable_t>> || + std::is_floating_point_v< + remove_nullable_t>>, + "Values inside the aggregation must be numerical."); static constexpr bool is_aggregation = true; static constexpr bool is_column = true; static constexpr bool is_operation = false; - using Name = Literal<_name>; - using Type = rfl::field_type_t<_name, StructType>; + using Name = Nothing; + using Type = + typename MakeField>::Type; - dynamic::SelectFrom::Field operator()(const auto&) const { + dynamic::SelectFrom::Field operator()(const auto& _val) const { using DynamicAggregationType = dynamic_aggregation_t<_agg>; - return dynamic::SelectFrom::Field{ - dynamic::Operation{.val = dynamic::Aggregation{DynamicAggregationType{ - .val = dynamic::Column{.name = _name.str()}}}}}; + return dynamic::SelectFrom::Field{dynamic::Operation{ + .val = dynamic::Aggregation{DynamicAggregationType{ + .val = Ref::make( + MakeField>{}( + _val.val) + .val)}}}}; } }; @@ -145,8 +144,7 @@ struct MakeField>> { return dynamic::SelectFrom::Field{dynamic::Operation{ .val = dynamic::Aggregation{dynamic::Aggregation::Count{ .val = dynamic::Column{.name = _name.str()}, - .distinct = _agg.distinct}}, - }}; + .distinct = _agg.distinct}}}}; } }; diff --git a/include/sqlgen/transpilation/to_transpilation_type.hpp b/include/sqlgen/transpilation/to_transpilation_type.hpp index eecf39ab..7c4b2432 100644 --- a/include/sqlgen/transpilation/to_transpilation_type.hpp +++ b/include/sqlgen/transpilation/to_transpilation_type.hpp @@ -4,8 +4,6 @@ #include #include -#include "Aggregation.hpp" -#include "AggregationOp.hpp" #include "Value.hpp" namespace sqlgen::transpilation { @@ -34,13 +32,6 @@ struct ToTranspilationType { Type operator()(const char* _val) const noexcept { return make_value(_val); } }; -template -struct ToTranspilationType> { - using Type = Aggregation<_agg, _ValueType>; - - Type operator()(const Type& _val) const noexcept { return _val; } -}; - template auto to_transpilation_type(const T& _t) { return ToTranspilationType>{}(_t); diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 3076b94d..1afa3ffb 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -5,6 +5,8 @@ #include #include +#include "Aggregation.hpp" +#include "AggregationOp.hpp" #include "Col.hpp" #include "Desc.hpp" #include "Operation.hpp" @@ -20,6 +22,11 @@ namespace sqlgen::transpilation { template struct Underlying; +template +struct Underlying> { + using Type = typename Underlying>::Type; +}; + template struct Underlying> { static_assert(all_columns_exist>(), "All columns must exist."); diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 983d2433..99cda641 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -75,7 +75,7 @@ std::string aggregation_to_sql( using Type = std::remove_cvref_t; std::stringstream stream; if constexpr (std::is_same_v) { - stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "AVG(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { const auto val = @@ -84,13 +84,13 @@ std::string aggregation_to_sql( stream << "COUNT(" << val << ")"; } else if constexpr (std::is_same_v) { - stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "MAX(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { - stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "MIN(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { - stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "SUM(" << operation_to_sql(*_agg.val) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index 739aa01a..d55b45e8 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -53,7 +53,7 @@ std::string aggregation_to_sql( using Type = std::remove_cvref_t; std::stringstream stream; if constexpr (std::is_same_v) { - stream << "AVG(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "AVG(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { const auto val = @@ -62,13 +62,13 @@ std::string aggregation_to_sql( stream << "COUNT(" << val << ")"; } else if constexpr (std::is_same_v) { - stream << "MAX(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "MAX(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { - stream << "MIN(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "MIN(" << operation_to_sql(*_agg.val) << ")"; } else if constexpr (std::is_same_v) { - stream << "SUM(" << column_or_value_to_sql(_agg.val) << ")"; + stream << "SUM(" << operation_to_sql(*_agg.val) << ")"; } else { static_assert(rfl::always_false_v, "Not all cases were covered."); diff --git a/tests/postgres/test_group_by_with_operations.cpp b/tests/postgres/test_group_by_with_operations.cpp index 43925c36..04bc1b93 100644 --- a/tests/postgres/test_group_by_with_operations.cpp +++ b/tests/postgres/test_group_by_with_operations.cpp @@ -41,12 +41,16 @@ TEST(postgres, test_group_by) { std::string last_name; std::string last_name_trimmed; double avg_age; + double max_age_plus_one; + double min_age_plus_one; }; const auto get_children = - select_from("last_name"_c, - trim("last_name"_c) | as<"last_name_trimmed">, - avg("age"_c) | as<"avg_age">) | + select_from( + "last_name"_c, trim("last_name"_c).as<"last_name_trimmed">(), + max(cast("age"_c) + 1.0).as<"max_age_plus_one">(), + (min(cast("age"_c)) + 1.0).as<"min_age_plus_one">(), + round(avg(cast("age"_c))).as<"avg_age">()) | where("age"_c < 18) | group_by("last_name"_c) | to>; const auto children = postgres::connect(credentials) @@ -59,6 +63,8 @@ TEST(postgres, test_group_by) { EXPECT_EQ(children.at(0).last_name, "Simpson"); EXPECT_EQ(children.at(0).last_name_trimmed, "Simpson"); EXPECT_EQ(children.at(0).avg_age, 6.0); + EXPECT_EQ(children.at(0).max_age_plus_one, 11.0); + EXPECT_EQ(children.at(0).min_age_plus_one, 1.0); } } // namespace test_group_by_with_operations diff --git a/tests/sqlite/test_group_by_with_operations.cpp b/tests/sqlite/test_group_by_with_operations.cpp new file mode 100644 index 00000000..833b80ec --- /dev/null +++ b/tests/sqlite/test_group_by_with_operations.cpp @@ -0,0 +1,62 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_group_by_with_operations { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_group_by_with_operations) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + using namespace sqlgen; + + struct Children { + std::string last_name; + std::string last_name_trimmed; + double avg_age; + double max_age_plus_one; + double min_age_plus_one; + }; + + const auto get_children = + select_from( + "last_name"_c, trim("last_name"_c).as<"last_name_trimmed">(), + max(cast("age"_c) + 1.0).as<"max_age_plus_one">(), + (min(cast("age"_c)) + 1.0).as<"min_age_plus_one">(), + round(avg(cast("age"_c))).as<"avg_age">()) | + where("age"_c < 18) | group_by("last_name"_c) | to>; + + const auto children = sqlite::connect() + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_children) + .value(); + + EXPECT_EQ(children.size(), 1); + EXPECT_EQ(children.at(0).last_name, "Simpson"); + EXPECT_EQ(children.at(0).last_name_trimmed, "Simpson"); + EXPECT_EQ(children.at(0).avg_age, 6.0); + EXPECT_EQ(children.at(0).max_age_plus_one, 11.0); + EXPECT_EQ(children.at(0).min_age_plus_one, 1.0); +} + +} // namespace test_group_by_with_operations +