Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ SET "first_name" = "last_name"
WHERE "age" > 18;
```

### Setting Nullable Values

Columns can be set using std::optional<T> values. Passing std::nullopt will set the column to NULL in the database, and passing a std::optional<T> 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<Person>("age"_c.set(std::nullopt)) |
where("first_name"_c == "Hugo");

// set to a value
const auto query2 = update<Person>("age"_c.set(std::optional<int>(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
Expand Down
4 changes: 3 additions & 1 deletion include/sqlgen/dynamic/Value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct Integer {
int64_t val;
};

struct Null {};

struct String {
std::string val;
};
Expand All @@ -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;
};
Expand Down
5 changes: 3 additions & 2 deletions include/sqlgen/transpilation/to_sets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -30,8 +31,8 @@ struct ToSet<T, Set<transpilation::Col<_name>, ToType>> {
static_assert(
all_columns_exist<T, transpilation::Col<_name>>(),
"At least one column referenced in your SET query does not exist.");
static_assert(std::is_convertible_v<underlying_t<T, Col<_name>>,
underlying_t<T, Value<ToType>>>,
static_assert(std::is_convertible_v<underlying_t<T, Value<ToType>>,
underlying_t<T, Col<_name>>>,
"Must be convertible.");

dynamic::Update::Set operator()(const auto& _set) const {
Expand Down
14 changes: 13 additions & 1 deletion include/sqlgen/transpilation/to_value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#include <rfl.hpp>
#include <type_traits>

#include "../dynamic/Type.hpp"
#include "../dynamic/Value.hpp"
#include "Value.hpp"
#include "has_reflection_method.hpp"
#include "is_nullable.hpp"

namespace sqlgen::transpilation {

Expand All @@ -17,7 +19,17 @@ template <class T>
struct ToValue {
dynamic::Value operator()(const T& _t) const {
using Type = std::remove_cvref_t<T>;
if constexpr (std::is_floating_point_v<Type>) {
if constexpr (is_nullable_v<Type>) {
if (!_t) {
return dynamic::Value{dynamic::Null{}};
}
return ToValue<std::remove_cvref_t<decltype(*_t)>>{}(*_t);

} else if constexpr (std::is_same_v<Type, std::nullopt_t> ||
std::is_same_v<Type, std::nullptr_t>) {
return dynamic::Value{dynamic::Null{}};

} else if constexpr (std::is_floating_point_v<Type>) {
return dynamic::Value{dynamic::Float{.val = static_cast<double>(_t)}};

} else if constexpr (std::is_same_v<Type, bool>) {
Expand Down
3 changes: 3 additions & 0 deletions src/sqlgen/duckdb/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type, dynamic::Null>) {
return "NULL";

} else if constexpr (std::is_same_v<Type, dynamic::Timestamp>) {
return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")";

Expand Down
3 changes: 3 additions & 0 deletions src/sqlgen/mysql/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ std::string column_or_value_to_sql(
if constexpr (std::is_same_v<Type, dynamic::String>) {
return "'" + escape_single_quote(_v.val) + "'";

} else if constexpr (std::is_same_v<Type, dynamic::Null>) {
return "NULL";

} else if constexpr (std::is_same_v<Type, dynamic::Duration>) {
const auto unit =
_v.unit == dynamic::TimeUnit::milliseconds
Expand Down
3 changes: 3 additions & 0 deletions src/sqlgen/postgres/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type, dynamic::Null>) {
return "NULL";

} else if constexpr (std::is_same_v<Type, dynamic::Timestamp>) {
return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")";

Expand Down
3 changes: 3 additions & 0 deletions src/sqlgen/sqlite/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ std::string column_or_value_to_sql(
rfl::enum_to_string(_v.unit) + "'";
}

} else if constexpr (std::is_same_v<Type, dynamic::Null>) {
return "NULL";

} else if constexpr (std::is_same_v<Type, dynamic::Timestamp>) {
return std::to_string(_v.seconds_since_unix);

Expand Down
57 changes: 57 additions & 0 deletions tests/duckdb/test_update_with_optional.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include <gtest/gtest.h>

#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/duckdb.hpp>
#include <vector>

namespace test_update_with_optional {

struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};

TEST(duckdb, test_update_with_optional) {
const auto people1 = std::vector<Person>(
{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<Person>("first_name"_c.set("last_name"_c),
"age"_c.set(std::nullopt)) |
where("first_name"_c == "Hugo");

const auto query2 =
update<Person>("age"_c.set(50)) | where("first_name"_c == "Homer");

const auto query3 = update<Person>("age"_c.set(std::optional<int>(11))) |
where("first_name"_c == "Bart");

query1(conn).and_then(query2).and_then(query3).value();

const auto people2 =
(sqlgen::read<std::vector<Person>> | 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
67 changes: 67 additions & 0 deletions tests/mysql/test_update_with_optional.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY

#include <gtest/gtest.h>

#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/mysql.hpp>
#include <vector>

namespace test_update_with_optional {

struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};

TEST(mysql, test_update_with_optional) {
const auto people1 = std::vector<Person>(
{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<Person> | if_exists);

sqlgen::write(conn, people1);

const auto query1 = update<Person>("first_name"_c.set("last_name"_c),
"age"_c.set(std::nullopt)) |
where("first_name"_c == "Hugo");

const auto query2 =
update<Person>("age"_c.set(50)) | where("first_name"_c == "Homer");

const auto query3 = update<Person>("age"_c.set(std::optional<int>(11))) |
where("first_name"_c == "Bart");

query1(conn).and_then(query2).and_then(query3).value();

const auto people2 =
(sqlgen::read<std::vector<Person>> | 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
67 changes: 67 additions & 0 deletions tests/postgres/test_update_with_optional.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY

#include <gtest/gtest.h>

#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/postgres.hpp>
#include <vector>

namespace test_update_with_optional {

struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};

TEST(postgres, test_update_with_optional) {
const auto people1 = std::vector<Person>(
{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<Person> | if_exists);

sqlgen::write(conn, people1);

const auto query1 = update<Person>("first_name"_c.set("last_name"_c),
"age"_c.set(std::nullopt)) |
where("first_name"_c == "Hugo");

const auto query2 =
update<Person>("age"_c.set(50)) | where("first_name"_c == "Homer");

const auto query3 = update<Person>("age"_c.set(std::optional<int>(11))) |
where("first_name"_c == "Bart");

query1(conn).and_then(query2).and_then(query3).value();

const auto people2 =
(sqlgen::read<std::vector<Person>> | 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
57 changes: 57 additions & 0 deletions tests/sqlite/test_update_with_optional.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include <gtest/gtest.h>

#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/sqlite.hpp>
#include <vector>

namespace test_update_with_optional {

struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
std::optional<int> age;
};

TEST(sqlite, test_update_with_optional) {
const auto people1 = std::vector<Person>(
{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<Person>("first_name"_c.set("last_name"_c),
"age"_c.set(std::nullopt)) |
where("first_name"_c == "Hugo");

const auto query2 =
update<Person>("age"_c.set(50)) | where("first_name"_c == "Homer");

const auto query3 = update<Person>("age"_c.set(std::optional<int>(11))) |
where("first_name"_c == "Bart");

query1(conn).and_then(query2).and_then(query3).value();

const auto people2 =
(sqlgen::read<std::vector<Person>> | 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
Loading