diff --git a/docs/README.md b/docs/README.md index 773c1e8..240b33e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,16 +5,19 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab ## Core Concepts - [Defining Tables](defining_tables.md) - How to define tables using C++ structs -- [sqlgen::Result](result.md) - How sqlgen handles errors and results -- [sqlgen::PrimaryKey](primary_key.md) - How to define primary keys in sqlgen +- [sqlgen::col](col.md) - How to represent columns in queries - [sqlgen::Flatten](flatten.md) - How to "inherit" fields from other structs +- [sqlgen::PrimaryKey](primary_key.md) - How to define primary keys in sqlgen +- [sqlgen::Result](result.md) - How sqlgen handles errors and results - [sqlgen::to_sql](to_sql.md) - How to transpile C++ operations to dialect-specific SQL -## Database Operations +## Database I/O - [sqlgen::read](reading.md) - How to read data from a database - [sqlgen::write](writing.md) - How to write data to a database +## Database Operations + - [sqlgen::create_index](create_index.md) - How to create an index on a table - [sqlgen::create_table](create_table.md) - How to create a new table - [sqlgen::delete_from](delete_from.md) - How to delete data from a table @@ -23,18 +26,20 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::insert](insert.md) - How to insert data within transactions - [sqlgen::update](update.md) - How to update data in a table -- [Transactions](transactions.md) - How to use transactions for atomic operations -- [Connection Pool](connection_pool.md) - How to manage database connections efficiently - ## Data Types and Validation +- [sqlgen::Pattern](pattern.md) - How to add regex pattern validation to avoid SQL injection - [sqlgen::Timestamp](timestamp.md) - How timestamps work in sqlgen - [sqlgen::Varchar](varchar.md) - How varchars work in sqlgen -- [sqlgen::Pattern](pattern.md) - How to add regex pattern validation to avoid SQL injection + +## Other concepts + +- [Connection Pool](connection_pool.md) - How to manage database connections efficiently +- [Transactions](transactions.md) - How to use transactions for atomic operations ## Supported Databases -- [PostgreSQL](postgres.md) - How to interact with PostgreSQL and compatible databases (Redshift, Aurora, Greenplum) +- [PostgreSQL](postgres.md) - How to interact with PostgreSQL and compatible databases (Redshift, Aurora, Greenplum, CockroachDB, ...) - [SQLite](sqlite.md) - How to interact with SQLite3 For installation instructions, quick start guide, and usage examples, please refer to the [main README](../README.md). diff --git a/docs/col.md b/docs/col.md new file mode 100644 index 0000000..54086b7 --- /dev/null +++ b/docs/col.md @@ -0,0 +1,195 @@ +# `sqlgen::col` + +`sqlgen::col` provides a type-safe way to reference and manipulate database columns in SQL queries. It enables building complex SQL conditions, ordering, and updates through a fluent C++ interface. + +## Usage + +### Basic Definition + +Reference a column using the string literal operator: + +```cpp +using namespace sqlgen; + +// Using string literal operator +const auto age_col = "age"_c; + +// Using col template function +const auto name_col = col<"first_name">; +``` + +### Column Operations + +#### Comparison Operations + +Compare columns with values or other columns: + +```cpp +using namespace sqlgen; + +// Compare with value +const auto query1 = read> | where("age"_c > 18); + +// Compare with another column +const auto query2 = read> | + where("age"_c > "id"_c); +``` + +This generates SQL like: + +```sql +-- For query1 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "age" > 18; + +-- For query2 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "age" > "id"; +``` + +#### NULL Operations + +Check for NULL or NOT NULL values: + +```cpp +using namespace sqlgen; + +// Find records where age is NULL +const auto query1 = read> | + where("age"_c.is_null()); + +// Find records where age is NOT NULL +const auto query2 = read> | + where("age"_c.is_not_null()); +``` + +This generates SQL like: + +```sql +-- For query1 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "age" IS NULL; + +-- For query2 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "age" IS NOT NULL; +``` + +#### Pattern Matching + +Use LIKE and NOT LIKE for pattern matching: + +```cpp +using namespace sqlgen; + +// Find names starting with 'H' +const auto query1 = read> | + where("first_name"_c.like("H%")); + +// Find names not starting with 'H' +const auto query2 = read> | + where("first_name"_c.not_like("H%")); +``` + +This generates SQL like: + +```sql +-- For query1 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "first_name" LIKE 'H%'; + +-- For query2 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +WHERE "first_name" NOT LIKE 'H%'; +``` + +#### Ordering + +Specify column ordering in queries: + +```cpp +using namespace sqlgen; + +// Order by age ascending +const auto query1 = read> | + order_by("age"_c); + +// Order by age descending +const auto query2 = read> | + order_by("age"_c.desc()); + +// Multiple columns with mixed ordering +const auto query3 = read> | + order_by("last_name"_c, "first_name"_c.desc()); +``` + +This generates SQL like: + +```sql +-- For query1 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +ORDER BY "age"; + +-- For query2 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +ORDER BY "age" DESC; + +-- For query3 +SELECT "id", "first_name", "last_name", "age" +FROM "Person" +ORDER BY "last_name", "first_name" DESC; +``` + +#### Updates + +Set column values in UPDATE statements: + +```cpp +using namespace sqlgen; + +// Update a single column +const auto query1 = update("age"_c.set(46)); + +// Update multiple columns +const auto query2 = update( + "first_name"_c.set("last_name"_c), + "age"_c.set(100) +) | where("first_name"_c == "Hugo"); +``` + +This generates SQL like: + +```sql +-- For query1 +UPDATE "Person" SET "age" = 46; + +-- For query2 +UPDATE "Person" +SET "first_name" = "last_name", "age" = 100 +WHERE "first_name" = 'Hugo'; +``` + +## Type Safety + +`sqlgen::col` class provides compile-time type safety: + +- Column names are validated at compile time using string literals +- SQL injection is prevented through proper escaping and parameterization + +## Notes + +- The class supports all standard SQL comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` +- Column names are automatically quoted in generated SQL +- The class is designed to be used with the query builder interface +- All operations are composable and can be chained together +- The class supports both value and column-to-column comparisons +- String literals are automatically converted to the appropriate SQL type +- The class is thread-safe and has no mutable state \ No newline at end of file diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index 7d3c307..90c9e73 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -22,13 +22,37 @@ struct Col { /// Returns the column name. std::string name() const noexcept { return Name().str(); } - /// Defines a SET clause in an UPDATE statement. + /// Returns an IS NULL condition. + auto is_null() const noexcept { + return transpilation::make_condition( + transpilation::conditions::is_null(*this)); + } + + /// Returns a IS NOT NULL condition. + auto is_not_null() const noexcept { + return transpilation::make_condition( + transpilation::conditions::is_not_null(*this)); + } + + /// Returns a LIKE condition. + auto like(const std::string& _pattern) const noexcept { + return transpilation::make_condition( + transpilation::conditions::like(*this, _pattern)); + } + + /// Returns a NOT LIKE condition. + auto not_like(const std::string& _pattern) const noexcept { + return transpilation::make_condition( + transpilation::conditions::not_like(*this, _pattern)); + } + + /// Returns a SET clause in an UPDATE statement. template auto set(const T& _to) const noexcept { return transpilation::Set, std::remove_cvref_t>{.to = _to}; } - /// Defines a SET clause in an UPDATE statement. + /// Returns a SET clause in an UPDATE statement. auto set(const char* _to) const noexcept { return transpilation::Set, std::string>{.to = _to}; } diff --git a/include/sqlgen/dynamic/Condition.hpp b/include/sqlgen/dynamic/Condition.hpp index 9ceb1ec..9ffd2a9 100644 --- a/include/sqlgen/dynamic/Condition.hpp +++ b/include/sqlgen/dynamic/Condition.hpp @@ -30,9 +30,12 @@ struct Condition { ColumnOrValue op2; }; - struct NotEqual { - Column op1; - ColumnOrValue op2; + struct IsNotNull { + Column op; + }; + + struct IsNull { + Column op; }; struct LesserEqual { @@ -45,14 +48,30 @@ struct Condition { ColumnOrValue op2; }; + struct Like { + Column op; + dynamic::Value pattern; + }; + + struct NotEqual { + Column op1; + ColumnOrValue op2; + }; + + struct NotLike { + Column op; + dynamic::Value pattern; + }; + struct Or { Ref cond1; Ref cond2; }; using ReflectionType = - rfl::TaggedUnion<"what", And, Equal, GreaterEqual, GreaterThan, NotEqual, - LesserEqual, LesserThan, Or>; + rfl::TaggedUnion<"what", And, Equal, GreaterEqual, GreaterThan, IsNull, + IsNotNull, LesserEqual, LesserThan, Like, NotEqual, + NotLike, Or>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/transpilation/conditions.hpp b/include/sqlgen/transpilation/conditions.hpp index 502b48a..9f87c65 100644 --- a/include/sqlgen/transpilation/conditions.hpp +++ b/include/sqlgen/transpilation/conditions.hpp @@ -45,16 +45,24 @@ auto greater_than(const OpType1& _op1, const OpType2& _op2) { std::remove_cvref_t>{.op1 = _op1, .op2 = _op2}; } -template -struct NotEqual { - OpType1 op1; - OpType2 op2; +template +struct IsNull { + OpType op; }; -template -auto not_equal(const OpType1& _op1, const OpType2& _op2) { - return NotEqual, std::remove_cvref_t>{ - .op1 = _op1, .op2 = _op2}; +template +auto is_null(const OpType& _op) { + return IsNull>{.op = _op}; +} + +template +struct IsNotNull { + OpType op; +}; + +template +auto is_not_null(const OpType& _op) { + return IsNotNull>{.op = _op}; } template @@ -81,6 +89,40 @@ auto lesser_than(const OpType1& _op1, const OpType2& _op2) { .op1 = _op1, .op2 = _op2}; } +template +struct Like { + OpType op; + std::string pattern; +}; + +template +auto like(const OpType& _op, const std::string& _pattern) { + return Like>{.op = _op, .pattern = _pattern}; +} + +template +struct NotEqual { + OpType1 op1; + OpType2 op2; +}; + +template +auto not_equal(const OpType1& _op1, const OpType2& _op2) { + return NotEqual, std::remove_cvref_t>{ + .op1 = _op1, .op2 = _op2}; +} + +template +struct NotLike { + OpType op; + std::string pattern; +}; + +template +auto not_like(const OpType& _op, const std::string& _pattern) { + return NotLike>{.op = _op, .pattern = _pattern}; +} + template struct Or { CondType1 cond1; diff --git a/include/sqlgen/transpilation/to_condition.hpp b/include/sqlgen/transpilation/to_condition.hpp index 35d34b4..c0aa10d 100644 --- a/include/sqlgen/transpilation/to_condition.hpp +++ b/include/sqlgen/transpilation/to_condition.hpp @@ -168,6 +168,37 @@ struct ToCondition, Value>> { } }; +template +struct ToCondition>> { + static_assert(all_columns_exist>(), "All columns must exist."); + + 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)}}; + } +}; + +template +struct ToCondition>> { + static_assert(all_columns_exist>(), "All columns must exist."); + + dynamic::Condition operator()(const auto&) 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()}}}; + } +}; + template struct ToCondition, Col<_name2>>> { @@ -194,6 +225,17 @@ struct ToCondition, Value>> { } }; +template +struct ToCondition>> { + static_assert(all_columns_exist>(), "All columns must exist."); + + 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)}}; + } +}; + template struct ToCondition> { dynamic::Condition operator()(const auto& _cond) const { diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 3279173..fd4261d 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -30,6 +30,8 @@ std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept; std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept; +std::string escape_single_quote(const std::string& _str) noexcept; + std::vector get_primary_keys( const dynamic::CreateTable& _stmt) noexcept; @@ -63,7 +65,7 @@ std::string column_or_value_to_sql( const auto handle_value = [](const auto& _v) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - return "'" + _v.val + "'"; + return "'" + escape_single_quote(_v.val) + "'"; } else { return std::to_string(_v.val); } @@ -105,9 +107,11 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << column_or_value_to_sql(_condition.op1) << " > " << column_or_value_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); + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op) << " IS NULL"; + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op) << " IS NOT NULL"; } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_condition.op1) @@ -117,12 +121,24 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << column_or_value_to_sql(_condition.op1) << " < " << column_or_value_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); + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op1) + << " != " << column_or_value_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); + } else if constexpr (std::is_same_v) { stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" << condition_to_sql(*_condition.cond2) << ")"; } else { - static_assert(rfl::always_false_v, "Not all cases where covered."); + static_assert(rfl::always_false_v, "Not all cases were covered."); } return stream.str(); @@ -246,6 +262,10 @@ std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept { return stream.str(); } +std::string escape_single_quote(const std::string& _str) noexcept { + return internal::strings::replace_all(_str, "'", "''"); +} + std::vector get_primary_keys( const dynamic::CreateTable& _stmt) noexcept { using namespace std::ranges::views; diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index db3ae5a..19563ce 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -26,6 +26,8 @@ std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept; std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept; +std::string escape_single_quote(const std::string& _str) noexcept; + template std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept; @@ -44,7 +46,7 @@ std::string column_or_value_to_sql( const auto handle_value = [](const auto& _v) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { - return "'" + _v.val + "'"; + return "'" + escape_single_quote(_v.val) + "'"; } else { return std::to_string(_v.val); } @@ -92,9 +94,11 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << column_or_value_to_sql(_condition.op1) << " > " << column_or_value_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); + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op) << " IS NULL"; + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op) << " IS NOT NULL"; } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_condition.op1) @@ -104,12 +108,24 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept { stream << column_or_value_to_sql(_condition.op1) << " < " << column_or_value_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); + + } else if constexpr (std::is_same_v) { + stream << column_or_value_to_sql(_condition.op1) + << " != " << column_or_value_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); + } else if constexpr (std::is_same_v) { stream << "(" << condition_to_sql(*_condition.cond1) << ") OR (" << condition_to_sql(*_condition.cond2) << ")"; } else { - static_assert(rfl::always_false_v, "Not all cases where covered."); + static_assert(rfl::always_false_v, "Not all cases were covered."); } return stream.str(); @@ -220,6 +236,10 @@ std::string drop_to_sql(const dynamic::Drop& _stmt) noexcept { return stream.str(); } +std::string escape_single_quote(const std::string& _str) noexcept { + return internal::strings::replace_all(_str, "'", "''"); +} + template std::string insert_or_write_to_sql(const InsertOrWrite& _stmt) noexcept { using namespace std::ranges::views; diff --git a/tests/postgres/test_is_null.cpp b/tests/postgres/test_is_null.cpp new file mode 100644 index 0000000..760750f --- /dev/null +++ b/tests/postgres/test_is_null.cpp @@ -0,0 +1,62 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_is_null { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(postgres, test_is_null) { + const auto people1 = std::vector( + {Person{.id = 0, .first_name = "Homer", .last_name = "Simpson"}, + 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"}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + const auto conn = postgres::connect(credentials); + + const auto people2 = + conn.and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("age"_c.is_null()) | order_by("first_name"_c.desc())) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + where("age"_c.is_not_null()) | order_by("age"_c)) + .value(); + + const std::string expected1 = + R"([{"id":4,"first_name":"Hugo","last_name":"Simpson"},{"id":0,"first_name":"Homer","last_name":"Simpson"}])"; + + const std::string expected2 = + 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), expected1); + EXPECT_EQ(rfl::json::write(people3), expected2); +} + +} // namespace test_is_null + +#endif diff --git a/tests/postgres/test_is_null_dry.cpp b/tests/postgres/test_is_null_dry.cpp new file mode 100644 index 0000000..7acc501 --- /dev/null +++ b/tests/postgres/test_is_null_dry.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_is_null_dry { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(postgres, test_is_null_dry) { + using namespace sqlgen; + + const auto sql = postgres::to_sql(sqlgen::read> | + where("age"_c.is_null()) | + order_by("first_name"_c.desc())); + + const std::string expected = + R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "age" IS NULL ORDER BY "first_name" DESC;)"; + + EXPECT_EQ(sql, expected); +} + +} // namespace test_is_null_dry + diff --git a/tests/postgres/test_like.cpp b/tests/postgres/test_like.cpp new file mode 100644 index 0000000..34a2d33 --- /dev/null +++ b/tests/postgres/test_like.cpp @@ -0,0 +1,72 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include + +namespace test_like { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(postgres, test_like) { + 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 = postgres::connect(credentials); + + const auto people2 = + conn.and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.like("H%")) | order_by("age"_c)) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + where("first_name"_c.not_like("H%")) | order_by("age"_c)) + .value(); + + const auto people4 = + conn.and_then(sqlgen::read> | + where("first_name"_c.like("O'Reilly")) | order_by("age"_c)) + .value(); + + const std::string expected1 = + R"([{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10},{"id":0,"first_name":"Homer","last_name":"Simpson","age":45}])"; + + const std::string expected2 = + 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}])"; + + const std::string expected3 = R"([])"; + + EXPECT_EQ(rfl::json::write(people2), expected1); + EXPECT_EQ(rfl::json::write(people3), expected2); + EXPECT_EQ(rfl::json::write(people4), expected3); +} + +} // namespace test_like + +#endif diff --git a/tests/postgres/test_like_dry.cpp b/tests/postgres/test_like_dry.cpp new file mode 100644 index 0000000..c43aa3c --- /dev/null +++ b/tests/postgres/test_like_dry.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_like_dry { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(postgres, test_like_dry) { + using namespace sqlgen; + + const auto sql = + postgres::to_sql(sqlgen::read> | + where("first_name"_c.like("H%")) | order_by("age"_c)); + + const std::string expected = + R"(SELECT "id", "first_name", "last_name", "age" FROM "Person" WHERE "first_name" LIKE 'H%' ORDER BY "age";)"; + + EXPECT_EQ(sql, expected); +} + +} // namespace test_like_dry + diff --git a/tests/sqlite/test_is_null.cpp b/tests/sqlite/test_is_null.cpp new file mode 100644 index 0000000..ba54173 --- /dev/null +++ b/tests/sqlite/test_is_null.cpp @@ -0,0 +1,54 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_is_null { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + std::optional age; +}; + +TEST(sqlite, test_is_null) { + const auto people1 = std::vector( + {Person{.id = 0, .first_name = "Homer", .last_name = "Simpson"}, + 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"}}); + + using namespace sqlgen; + + const auto conn = sqlite::connect(); + + const auto people2 = + conn.and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("age"_c.is_null()) | order_by("first_name"_c.desc())) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + where("age"_c.is_not_null()) | order_by("age"_c)) + .value(); + + const std::string expected1 = + R"([{"id":4,"first_name":"Hugo","last_name":"Simpson"},{"id":0,"first_name":"Homer","last_name":"Simpson"}])"; + + const std::string expected2 = + 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), expected1); + EXPECT_EQ(rfl::json::write(people3), expected2); +} + +} // namespace test_is_null + diff --git a/tests/sqlite/test_like.cpp b/tests/sqlite/test_like.cpp new file mode 100644 index 0000000..5eff66a --- /dev/null +++ b/tests/sqlite/test_like.cpp @@ -0,0 +1,64 @@ + +#include + +#include +#include +#include +#include +#include + +namespace test_like { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + int age; +}; + +TEST(sqlite, test_like) { + 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}}); + + using namespace sqlgen; + + const auto conn = sqlite::connect(); + + const auto people2 = + conn.and_then(write(std::ref(people1))) + .and_then(sqlgen::read> | + where("first_name"_c.like("H%")) | order_by("age"_c)) + .value(); + + const auto people3 = + conn.and_then(sqlgen::read> | + where("first_name"_c.not_like("H%")) | order_by("age"_c)) + .value(); + + const auto people4 = + conn.and_then(sqlgen::read> | + where("first_name"_c.like("O'Reilly")) | order_by("age"_c)) + .value(); + + const std::string expected1 = + R"([{"id":4,"first_name":"Hugo","last_name":"Simpson","age":10},{"id":0,"first_name":"Homer","last_name":"Simpson","age":45}])"; + + const std::string expected2 = + 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}])"; + + const std::string expected3 = R"([])"; + + EXPECT_EQ(rfl::json::write(people2), expected1); + EXPECT_EQ(rfl::json::write(people3), expected2); + EXPECT_EQ(rfl::json::write(people4), expected3); +} + +} // namespace test_like +