diff --git a/docs/update.md b/docs/update.md index 802b9b9..46c8d83 100644 --- a/docs/update.md +++ b/docs/update.md @@ -98,6 +98,37 @@ SET "first_name" = "last_name" WHERE "age" > 18; ``` +### Setting Nullable Values + +Columns can be set using std::optional values. Passing std::nullopt will set the column to NULL in the database, and passing a std::optional with a value will set the column to that value. + +```cpp +using namespace sqlgen; +using namespace sqlgen::literals; + +// set to NULL +const auto query1 = update("age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); + +// set to a value +const auto query2 = update("age"_c.set(std::optional(11))) | + where("first_name"_c == "Bart"); + +query1(conn).and_then(query2).value(); +``` + +This generates the following SQL: + +```sql +UPDATE "Person" +SET "age" = NULL +WHERE "first_name" = 'Hugo'; + +UPDATE "Person" +SET "age" = 11 +WHERE "first_name" = 'Bart'; +``` + ## Example: Full Query Composition ```cpp diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index 586159b..35eb668 100644 --- a/include/sqlgen/dynamic/Value.hpp +++ b/include/sqlgen/dynamic/Value.hpp @@ -25,6 +25,8 @@ struct Integer { int64_t val; }; +struct Null {}; + struct String { std::string val; }; @@ -35,7 +37,7 @@ struct Timestamp { struct Value { using ReflectionType = rfl::TaggedUnion<"type", Duration, Boolean, Float, - Integer, String, Timestamp>; + Integer, Null, String, Timestamp>; const auto& reflection() const { return val; } ReflectionType val; }; diff --git a/include/sqlgen/transpilation/to_sets.hpp b/include/sqlgen/transpilation/to_sets.hpp index 3a071f8..e95aa75 100644 --- a/include/sqlgen/transpilation/to_sets.hpp +++ b/include/sqlgen/transpilation/to_sets.hpp @@ -15,6 +15,7 @@ #include "all_columns_exist.hpp" #include "get_schema.hpp" #include "get_tablename.hpp" +#include "remove_nullable_t.hpp" #include "to_condition.hpp" #include "to_sets.hpp" #include "to_value.hpp" @@ -30,8 +31,8 @@ struct ToSet, ToType>> { static_assert( all_columns_exist>(), "At least one column referenced in your SET query does not exist."); - static_assert(std::is_convertible_v>, - underlying_t>>, + static_assert(std::is_convertible_v>, + underlying_t>>, "Must be convertible."); dynamic::Update::Set operator()(const auto& _set) const { diff --git a/include/sqlgen/transpilation/to_value.hpp b/include/sqlgen/transpilation/to_value.hpp index 5ca5c55..a39a217 100644 --- a/include/sqlgen/transpilation/to_value.hpp +++ b/include/sqlgen/transpilation/to_value.hpp @@ -4,9 +4,11 @@ #include #include +#include "../dynamic/Type.hpp" #include "../dynamic/Value.hpp" #include "Value.hpp" #include "has_reflection_method.hpp" +#include "is_nullable.hpp" namespace sqlgen::transpilation { @@ -17,7 +19,17 @@ template struct ToValue { dynamic::Value operator()(const T& _t) const { using Type = std::remove_cvref_t; - if constexpr (std::is_floating_point_v) { + if constexpr (is_nullable_v) { + if (!_t) { + return dynamic::Value{dynamic::Null{}}; + } + return ToValue>{}(*_t); + + } else if constexpr (std::is_same_v || + std::is_same_v) { + return dynamic::Value{dynamic::Null{}}; + + } else if constexpr (std::is_floating_point_v) { return dynamic::Value{dynamic::Float{.val = static_cast(_t)}}; } else if constexpr (std::is_same_v) { diff --git a/src/sqlgen/duckdb/to_sql.cpp b/src/sqlgen/duckdb/to_sql.cpp index 3769f54..034aaed 100644 --- a/src/sqlgen/duckdb/to_sql.cpp +++ b/src/sqlgen/duckdb/to_sql.cpp @@ -145,6 +145,9 @@ std::string column_or_value_to_sql( return "INTERVAL '" + std::to_string(_v.val) + " " + rfl::enum_to_string(_v.unit) + "'"; + } else if constexpr (std::is_same_v) { + return "NULL"; + } else if constexpr (std::is_same_v) { return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")"; diff --git a/src/sqlgen/mysql/to_sql.cpp b/src/sqlgen/mysql/to_sql.cpp index 2346f91..4683a78 100644 --- a/src/sqlgen/mysql/to_sql.cpp +++ b/src/sqlgen/mysql/to_sql.cpp @@ -175,6 +175,9 @@ std::string column_or_value_to_sql( if constexpr (std::is_same_v) { return "'" + escape_single_quote(_v.val) + "'"; + } else if constexpr (std::is_same_v) { + return "NULL"; + } else if constexpr (std::is_same_v) { const auto unit = _v.unit == dynamic::TimeUnit::milliseconds diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index d2a4777..534c650 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -134,6 +134,9 @@ std::string column_or_value_to_sql( return "INTERVAL '" + std::to_string(_v.val) + " " + rfl::enum_to_string(_v.unit) + "'"; + } else if constexpr (std::is_same_v) { + return "NULL"; + } else if constexpr (std::is_same_v) { return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")"; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index c93da21..1a53328 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -136,6 +136,9 @@ std::string column_or_value_to_sql( rfl::enum_to_string(_v.unit) + "'"; } + } else if constexpr (std::is_same_v) { + return "NULL"; + } else if constexpr (std::is_same_v) { return std::to_string(_v.seconds_since_unix); diff --git a/tests/duckdb/test_update_with_optional.cpp b/tests/duckdb/test_update_with_optional.cpp new file mode 100644 index 0000000..2dbd87d --- /dev/null +++ b/tests/duckdb/test_update_with_optional.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_update_with_optional { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(duckdb, test_update_with_optional) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::duckdb::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query1 = update("first_name"_c.set("last_name"_c), + "age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); + + const auto query2 = + update("age"_c.set(50)) | where("first_name"_c == "Homer"); + + const auto query3 = update("age"_c.set(std::optional(11))) | + where("first_name"_c == "Bart"); + + query1(conn).and_then(query2).and_then(query3).value(); + + const auto people2 = + (sqlgen::read> | order_by("id"_c))(conn).value(); + + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":50},{"id":1,"first_name":"Bart","last_name":"Simpson","age":11},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Simpson","last_name":"Simpson"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_update_with_optional diff --git a/tests/mysql/test_update_with_optional.cpp b/tests/mysql/test_update_with_optional.cpp new file mode 100644 index 0000000..df6fd23 --- /dev/null +++ b/tests/mysql/test_update_with_optional.cpp @@ -0,0 +1,67 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_update_with_optional { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(mysql, test_update_with_optional) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto credentials = sqlgen::mysql::Credentials{.host = "localhost", + .user = "sqlgen", + .password = "password", + .dbname = "mysql"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = + sqlgen::mysql::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1); + + const auto query1 = update("first_name"_c.set("last_name"_c), + "age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); + + const auto query2 = + update("age"_c.set(50)) | where("first_name"_c == "Homer"); + + const auto query3 = update("age"_c.set(std::optional(11))) | + where("first_name"_c == "Bart"); + + query1(conn).and_then(query2).and_then(query3).value(); + + const auto people2 = + (sqlgen::read> | order_by("id"_c))(conn).value(); + + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":50},{"id":1,"first_name":"Bart","last_name":"Simpson","age":11},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Simpson","last_name":"Simpson"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_update_with_optional + +#endif diff --git a/tests/postgres/test_update_with_optional.cpp b/tests/postgres/test_update_with_optional.cpp new file mode 100644 index 0000000..ec1f2e7 --- /dev/null +++ b/tests/postgres/test_update_with_optional.cpp @@ -0,0 +1,67 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_update_with_optional { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(postgres, test_update_with_optional) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1); + + const auto query1 = update("first_name"_c.set("last_name"_c), + "age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); + + const auto query2 = + update("age"_c.set(50)) | where("first_name"_c == "Homer"); + + const auto query3 = update("age"_c.set(std::optional(11))) | + where("first_name"_c == "Bart"); + + query1(conn).and_then(query2).and_then(query3).value(); + + const auto people2 = + (sqlgen::read> | order_by("id"_c))(conn).value(); + + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":50},{"id":1,"first_name":"Bart","last_name":"Simpson","age":11},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Simpson","last_name":"Simpson"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_update_with_optional + +#endif diff --git a/tests/sqlite/test_update_with_optional.cpp b/tests/sqlite/test_update_with_optional.cpp new file mode 100644 index 0000000..6cae65d --- /dev/null +++ b/tests/sqlite/test_update_with_optional.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_update_with_optional { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(sqlite, test_update_with_optional) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}, + Person{ + .id = 4, .first_name = "Hugo", .last_name = "Simpson", .age = 10}}); + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1); + + using namespace sqlgen; + using namespace sqlgen::literals; + + const auto query1 = update("first_name"_c.set("last_name"_c), + "age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); + + const auto query2 = + update("age"_c.set(50)) | where("first_name"_c == "Homer"); + + const auto query3 = update("age"_c.set(std::optional(11))) | + where("first_name"_c == "Bart"); + + query1(conn).and_then(query2).and_then(query3).value(); + + const auto people2 = + (sqlgen::read> | order_by("id"_c))(conn).value(); + + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":50},{"id":1,"first_name":"Bart","last_name":"Simpson","age":11},{"id":2,"first_name":"Lisa","last_name":"Simpson","age":8},{"id":3,"first_name":"Maggie","last_name":"Simpson","age":0},{"id":4,"first_name":"Simpson","last_name":"Simpson"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_update_with_optional