From 3600ae8199bf52275dcec661deaab1011750d569 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Thu, 25 Dec 2025 16:04:21 +0100 Subject: [PATCH 1/8] Made sure the .set(...) works for nullable values --- include/sqlgen/transpilation/to_sets.hpp | 8 ++-- tests/sqlite/test_update_with_optional.cpp | 50 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 tests/sqlite/test_update_with_optional.cpp diff --git a/include/sqlgen/transpilation/to_sets.hpp b/include/sqlgen/transpilation/to_sets.hpp index 3a071f84..d1e6750e 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,9 +31,10 @@ 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>>, - "Must be convertible."); + static_assert( + std::is_convertible_v>>, + remove_nullable_t>>>, + "Must be convertible."); dynamic::Update::Set operator()(const auto& _set) const { return dynamic::Update::Set{ diff --git a/tests/sqlite/test_update_with_optional.cpp b/tests/sqlite/test_update_with_optional.cpp new file mode 100644 index 00000000..66057b83 --- /dev/null +++ b/tests/sqlite/test_update_with_optional.cpp @@ -0,0 +1,50 @@ +#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 query = + update("first_name"_c.set("last_name"_c), "age"_c.set(100)) | + where("first_name"_c == "Hugo"); + + query(conn).value(); + + const auto people2 = sqlgen::read>(conn).value(); + + const std::string expected = + R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"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","age":100}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_update_with_optional From 943247cec354ac76de70d18b3211bcb7dcc9b5a4 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 10:55:30 +0100 Subject: [PATCH 2/8] Made sure that setting to NULL values works --- include/sqlgen/dynamic/Value.hpp | 4 +++- include/sqlgen/transpilation/to_sets.hpp | 7 +++---- include/sqlgen/transpilation/to_value.hpp | 14 +++++++++++++- src/sqlgen/duckdb/to_sql.cpp | 3 +++ src/sqlgen/mysql/to_sql.cpp | 3 +++ src/sqlgen/postgres/to_sql.cpp | 3 +++ src/sqlgen/sqlite/to_sql.cpp | 3 +++ tests/sqlite/test_update_with_optional.cpp | 16 +++++++++++----- 8 files changed, 42 insertions(+), 11 deletions(-) diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index 586159ba..35eb6685 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 d1e6750e..e95aa75b 100644 --- a/include/sqlgen/transpilation/to_sets.hpp +++ b/include/sqlgen/transpilation/to_sets.hpp @@ -31,10 +31,9 @@ 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>>, - remove_nullable_t>>>, - "Must be convertible."); + static_assert(std::is_convertible_v>, + underlying_t>>, + "Must be convertible."); dynamic::Update::Set operator()(const auto& _set) const { return dynamic::Update::Set{ diff --git a/include/sqlgen/transpilation/to_value.hpp b/include/sqlgen/transpilation/to_value.hpp index 5ca5c556..a39a217c 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 3769f544..034aaedb 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 2346f91c..4683a78e 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 d2a47778..534c6506 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 c93da219..1a53328d 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/sqlite/test_update_with_optional.cpp b/tests/sqlite/test_update_with_optional.cpp index 66057b83..ce3a40be 100644 --- a/tests/sqlite/test_update_with_optional.cpp +++ b/tests/sqlite/test_update_with_optional.cpp @@ -33,16 +33,22 @@ TEST(sqlite, test_update_with_optional) { using namespace sqlgen; using namespace sqlgen::literals; - const auto query = - update("first_name"_c.set("last_name"_c), "age"_c.set(100)) | - where("first_name"_c == "Hugo"); + const auto query1 = update("first_name"_c.set("last_name"_c), + "age"_c.set(std::nullopt)) | + where("first_name"_c == "Hugo"); - query(conn).value(); + 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>(conn).value(); const std::string expected = - R"([{"id":0,"first_name":"Homer","last_name":"Simpson","age":45},{"id":1,"first_name":"Bart","last_name":"Simpson","age":10},{"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","age":100}])"; + 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); } From a181fe8ab831dbe5e022ed2369de92f1e536e7ca Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 10:59:22 +0100 Subject: [PATCH 3/8] Added more tests --- tests/duckdb/test_update_with_optional.cpp | 56 ++++++++++++++++++ tests/mysql/test_update_with_optional.cpp | 62 ++++++++++++++++++++ tests/postgres/test_update_with_optional.cpp | 62 ++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 tests/duckdb/test_update_with_optional.cpp create mode 100644 tests/mysql/test_update_with_optional.cpp create mode 100644 tests/postgres/test_update_with_optional.cpp diff --git a/tests/duckdb/test_update_with_optional.cpp b/tests/duckdb/test_update_with_optional.cpp new file mode 100644 index 00000000..3b5dede3 --- /dev/null +++ b/tests/duckdb/test_update_with_optional.cpp @@ -0,0 +1,56 @@ +#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>(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 00000000..6be0cff9 --- /dev/null +++ b/tests/mysql/test_update_with_optional.cpp @@ -0,0 +1,62 @@ +#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"}; + + const auto conn = + sqlgen::mysql::connect(credentials).and_then(drop | if_exists); + + 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>(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/postgres/test_update_with_optional.cpp b/tests/postgres/test_update_with_optional.cpp new file mode 100644 index 00000000..16bbbb50 --- /dev/null +++ b/tests/postgres/test_update_with_optional.cpp @@ -0,0 +1,62 @@ +#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"}; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + 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>(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 From bc9a38bea0aea33ad51e7a0773302acd429355d4 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 11:14:14 +0100 Subject: [PATCH 4/8] Fixed typo --- tests/mysql/test_update_with_optional.cpp | 6 +++--- tests/postgres/test_update_with_optional.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/mysql/test_update_with_optional.cpp b/tests/mysql/test_update_with_optional.cpp index 6be0cff9..9d39a8fc 100644 --- a/tests/mysql/test_update_with_optional.cpp +++ b/tests/mysql/test_update_with_optional.cpp @@ -31,14 +31,14 @@ TEST(mysql, test_update_with_optional) { .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); - 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"); diff --git a/tests/postgres/test_update_with_optional.cpp b/tests/postgres/test_update_with_optional.cpp index 16bbbb50..d6e3d78c 100644 --- a/tests/postgres/test_update_with_optional.cpp +++ b/tests/postgres/test_update_with_optional.cpp @@ -31,14 +31,14 @@ TEST(postgres, test_update_with_optional) { .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); - 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"); From 392d0e0ec01003df2cbe059599ab2cbbc2085ebe Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 11:31:29 +0100 Subject: [PATCH 5/8] Insert ORDER BY to make the tests more stable --- tests/duckdb/test_update_with_optional.cpp | 3 ++- tests/mysql/test_update_with_optional.cpp | 3 ++- tests/postgres/test_update_with_optional.cpp | 3 ++- tests/sqlite/test_update_with_optional.cpp | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/duckdb/test_update_with_optional.cpp b/tests/duckdb/test_update_with_optional.cpp index 3b5dede3..37ebaeca 100644 --- a/tests/duckdb/test_update_with_optional.cpp +++ b/tests/duckdb/test_update_with_optional.cpp @@ -45,7 +45,8 @@ TEST(duckdb, test_update_with_optional) { query1(conn).and_then(query2).and_then(query3).value(); - const auto people2 = sqlgen::read>(conn).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"}])"; diff --git a/tests/mysql/test_update_with_optional.cpp b/tests/mysql/test_update_with_optional.cpp index 9d39a8fc..18d3a51b 100644 --- a/tests/mysql/test_update_with_optional.cpp +++ b/tests/mysql/test_update_with_optional.cpp @@ -51,7 +51,8 @@ TEST(mysql, test_update_with_optional) { query1(conn).and_then(query2).and_then(query3).value(); - const auto people2 = sqlgen::read>(conn).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"}])"; diff --git a/tests/postgres/test_update_with_optional.cpp b/tests/postgres/test_update_with_optional.cpp index d6e3d78c..1fe86714 100644 --- a/tests/postgres/test_update_with_optional.cpp +++ b/tests/postgres/test_update_with_optional.cpp @@ -51,7 +51,8 @@ TEST(postgres, test_update_with_optional) { query1(conn).and_then(query2).and_then(query3).value(); - const auto people2 = sqlgen::read>(conn).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"}])"; diff --git a/tests/sqlite/test_update_with_optional.cpp b/tests/sqlite/test_update_with_optional.cpp index ce3a40be..6cae65de 100644 --- a/tests/sqlite/test_update_with_optional.cpp +++ b/tests/sqlite/test_update_with_optional.cpp @@ -45,7 +45,8 @@ TEST(sqlite, test_update_with_optional) { query1(conn).and_then(query2).and_then(query3).value(); - const auto people2 = sqlgen::read>(conn).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"}])"; From fb9ead6bb9d9a02ba3d34b59beef4b0e0f2c7fb3 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 11:43:00 +0100 Subject: [PATCH 6/8] Build dry tests only --- tests/mysql/test_update_with_optional.cpp | 4 ++++ tests/postgres/test_update_with_optional.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tests/mysql/test_update_with_optional.cpp b/tests/mysql/test_update_with_optional.cpp index 18d3a51b..df6fd23e 100644 --- a/tests/mysql/test_update_with_optional.cpp +++ b/tests/mysql/test_update_with_optional.cpp @@ -1,3 +1,5 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + #include #include @@ -61,3 +63,5 @@ TEST(mysql, test_update_with_optional) { } } // namespace test_update_with_optional + +#endif diff --git a/tests/postgres/test_update_with_optional.cpp b/tests/postgres/test_update_with_optional.cpp index 1fe86714..ec1f2e7d 100644 --- a/tests/postgres/test_update_with_optional.cpp +++ b/tests/postgres/test_update_with_optional.cpp @@ -1,3 +1,5 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + #include #include @@ -61,3 +63,5 @@ TEST(postgres, test_update_with_optional) { } } // namespace test_update_with_optional + +#endif From 8fd346107743054953e068249654f26389fb170f Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 11:50:03 +0100 Subject: [PATCH 7/8] Update the documentation --- docs/update.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/update.md b/docs/update.md index 802b9b9b..46c8d833 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 From 113a9278572f546b8af7e79b7d628ef58cd37a20 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Fri, 26 Dec 2025 12:05:06 +0100 Subject: [PATCH 8/8] Fixed final typo --- tests/duckdb/test_update_with_optional.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/duckdb/test_update_with_optional.cpp b/tests/duckdb/test_update_with_optional.cpp index 37ebaeca..2dbd87d2 100644 --- a/tests/duckdb/test_update_with_optional.cpp +++ b/tests/duckdb/test_update_with_optional.cpp @@ -46,7 +46,7 @@ TEST(duckdb, test_update_with_optional) { query1(conn).and_then(query2).and_then(query3).value(); const auto people2 = - (sqlgen::read> | order_by("id_c"))(conn).value(); + (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"}])";