Added delete_from

This commit is contained in:
Dr. Patrick Urbanke
2025-05-10 05:29:37 +02:00
parent d9f91b30f4
commit 982114240a
13 changed files with 368 additions and 5 deletions

View File

@@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS "People" (
"age" INTEGER NOT NULL
);
INSERT INTO "Person" ("first_name", "last_name", "age") VALUES (?, ?, ?);
INSERT INTO "People" ("first_name", "last_name", "age") VALUES (?, ?, ?);
```
## Retrieving data
@@ -87,7 +87,7 @@ The resulting SQL code:
```sql
SELECT "first_name", "last_name", "age"
FROM "Person"
FROM "People"
WHERE "age" < 18
ORDER BY "age", "first_name"
LIMIT 100;
@@ -116,10 +116,10 @@ sqlgen provides input validation to protect against SQL injection.
```cpp
// Safe query function using AlphaNumeric for filtering
std::vector<Person> get_people(const auto& conn,
std::vector<People> get_people(const auto& conn,
const sqlgen::AlphaNumeric& first_name) {
using namespace sqlgen;
const auto query = sqlgen::read<std::vector<Person>> |
const auto query = sqlgen::read<std::vector<People>> |
where("first_name"_c == first_name);
return query(conn).value();
}
@@ -132,6 +132,24 @@ Without `AlphaNumeric` validation, this code would be vulnerable to SQL injectio
get_people(conn, "Homer' OR '1'='1"); // Attempt to bypass filtering
```
## Deleting data
```cpp
using namespace sqlgen;
const auto query = delete_from<People> |
where("first_name"_c == "Hugo");
query(conn).value();
```
This generates the following SQL:
```sql
DELETE FROM "People"
WHERE "first_name" = 'Hugo';
```
## Installation
These three libraries are needed for PostgreSQL support:

102
docs/delete_from.md Normal file
View File

@@ -0,0 +1,102 @@
# `sqlgen::delete_from`
The `sqlgen::delete_from` interface provides a type-safe way to delete records from a SQL database. It supports composable query building with `where` clauses to specify which records should be deleted.
## Usage
### Basic Delete
Delete all records from a table:
```cpp
const auto conn = sqlgen::sqlite::connect("database.db");
sqlgen::delete_from<Person>(conn).value();
```
This generates the following SQL:
```sql
DELETE FROM "Person";
```
Note that `conn` is actually a connection wrapped into an `sqlgen::Result<...>`.
This means you can use monadic error handling and fit this into a single line:
```cpp
// sqlgen::Result<Nothing>
const auto result = sqlgen::sqlite::connect("database.db").and_then(
sqlgen::delete_from<Person>);
```
Please refer to the documentation on `sqlgen::Result<...>` for more information on error handling.
### With `where` clause
Delete specific records using a `where` clause:
```cpp
using namespace sqlgen;
const auto query = delete_from<Person> |
where("first_name"_c == "Hugo");
query(conn).value();
```
This generates the following SQL:
```sql
DELETE FROM "Person"
WHERE "first_name" = 'Hugo';
```
Note that `"..."_c` refers to the name of the column. If such a field does not
exist on the struct `Person`, the code will fail to compile.
You can also use monadic error handling here:
```cpp
using namespace sqlgen;
const auto query = delete_from<Person> |
where("first_name"_c == "Hugo");
// sqlgen::Result<Nothing>
const auto result = sqlite::connect("database.db").and_then(query);
```
## Example: Full Query Composition
```cpp
using namespace sqlgen;
const auto query = delete_from<Person> |
where("age"_c >= 18 and "last_name"_c == "Simpson");
const auto result = query(conn).value();
```
This generates the following SQL:
```sql
DELETE FROM "Person"
WHERE ("age" >= 18) AND ("last_name" = 'Simpson');
```
It is strongly recommended that you use `using namespace sqlgen`. However,
if you do not want to do that, you can rewrite the example above as follows:
```cpp
const auto query = sqlgen::delete_from<Person> |
sqlgen::where(sqlgen::col<"age"> >= 18 and sqlgen::col<"last_name"> == "Simpson");
const auto result = query(conn).value();
```
## Notes
- The `where` clause is optional - if omitted, all records will be deleted
- The `Result<Nothing>` type provides error handling; use `.value()` to extract the result (will throw an exception if there's an error) or handle errors as needed or refer to the documentation on results for other forms of error handling.
- `"..."_c` refers to the name of the column

View File

@@ -15,6 +15,7 @@
#include "sqlgen/Result.hpp"
#include "sqlgen/Varchar.hpp"
#include "sqlgen/col.hpp"
#include "sqlgen/delete_from.hpp"
#include "sqlgen/limit.hpp"
#include "sqlgen/order_by.hpp"
#include "sqlgen/patterns.hpp"

View File

@@ -0,0 +1,47 @@
#ifndef SQLGEN_DELETE_FROM_HPP_
#define SQLGEN_DELETE_FROM_HPP_
#include <type_traits>
#include "Connection.hpp"
#include "Ref.hpp"
#include "Result.hpp"
#include "transpilation/to_delete_from.hpp"
namespace sqlgen {
template <class ValueType, class WhereType>
Result<Nothing> delete_from_impl(const Ref<Connection>& _conn,
const WhereType& _where) {
const auto query =
transpilation::to_delete_from<ValueType, WhereType>(_where);
return _conn->execute(_conn->to_sql(query));
}
template <class ValueType, class WhereType>
Result<Nothing> delete_from_impl(const Result<Ref<Connection>>& _res,
const WhereType& _where) {
return _res.and_then([&](const auto& _conn) {
return delete_from_impl<ValueType, WhereType>(_conn, _where);
});
}
template <class ValueType, class WhereType = Nothing>
struct DeleteFrom {
Result<Nothing> operator()(const auto& _conn) const noexcept {
try {
return delete_from_impl<ValueType, WhereType>(_conn, where_);
} catch (std::exception& e) {
return error(e.what());
}
}
WhereType where_;
};
template <class ContainerType>
const auto delete_from = DeleteFrom<ContainerType>{};
} // namespace sqlgen
#endif

View File

@@ -0,0 +1,18 @@
#ifndef SQLGEN_DYNAMIC_DELETEFROM_HPP_
#define SQLGEN_DYNAMIC_DELETEFROM_HPP_
#include <optional>
#include "Condition.hpp"
#include "Table.hpp"
namespace sqlgen::dynamic {
struct DeleteFrom {
Table table;
std::optional<Condition> where = std::nullopt;
};
} // namespace sqlgen::dynamic
#endif

View File

@@ -4,12 +4,14 @@
#include <rfl.hpp>
#include "CreateTable.hpp"
#include "DeleteFrom.hpp"
#include "Insert.hpp"
#include "SelectFrom.hpp"
namespace sqlgen::dynamic {
using Statement = rfl::TaggedUnion<"stmt", CreateTable, Insert, SelectFrom>;
using Statement =
rfl::TaggedUnion<"stmt", CreateTable, DeleteFrom, Insert, SelectFrom>;
} // namespace sqlgen::dynamic

View File

@@ -0,0 +1,30 @@
#ifndef SQLGEN_TRANSPILATION_TO_DELETE_FROM_HPP_
#define SQLGEN_TRANSPILATION_TO_DELETE_FROM_HPP_
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "../Result.hpp"
#include "../dynamic/DeleteFrom.hpp"
#include "../dynamic/Table.hpp"
#include "get_schema.hpp"
#include "get_tablename.hpp"
#include "to_condition.hpp"
namespace sqlgen::transpilation {
template <class T, class WhereType>
requires std::is_class_v<std::remove_cvref_t<T>> &&
std::is_aggregate_v<std::remove_cvref_t<T>>
dynamic::DeleteFrom to_delete_from(const WhereType& _where) {
return dynamic::DeleteFrom{
.table =
dynamic::Table{.name = get_tablename<T>(), .schema = get_schema<T>()},
.where = to_condition<std::remove_cvref_t<T>>(_where)};
}
} // namespace sqlgen::transpilation
#endif

View File

@@ -5,9 +5,11 @@
#include "../CreateTable.hpp"
#include "../Insert.hpp"
#include "../delete_from.hpp"
#include "../dynamic/Statement.hpp"
#include "../read.hpp"
#include "to_create_table.hpp"
#include "to_delete_from.hpp"
#include "to_insert.hpp"
#include "to_select_from.hpp"
#include "value_t.hpp"
@@ -24,6 +26,13 @@ struct ToSQL<CreateTable<T>> {
}
};
template <class T, class WhereType>
struct ToSQL<DeleteFrom<T, WhereType>> {
dynamic::Statement operator()(const auto& _delete_from) const {
return to_delete_from<T>(_delete_from.where_);
}
};
template <class T>
struct ToSQL<Insert<T>> {
dynamic::Statement operator()(const auto&) const { return to_insert<T>(); }

View File

@@ -4,6 +4,7 @@
#include <type_traits>
#include "Result.hpp"
#include "delete_from.hpp"
#include "read.hpp"
#include "transpilation/Limit.hpp"
#include "transpilation/value_t.hpp"
@@ -15,6 +16,15 @@ struct Where {
ConditionType condition;
};
template <class ValueType, class WhereType, class ConditionType>
auto operator|(const DeleteFrom<ValueType, WhereType>& _d,
const Where<ConditionType>& _where) {
static_assert(std::is_same_v<WhereType, Nothing>,
"You cannot call where(...) twice (but you can apply more "
"than one condition by combining them with && or ||).");
return DeleteFrom<ValueType, ConditionType>{.where_ = _where.condition};
}
template <class ContainerType, class WhereType, class OrderByType,
class LimitType, class ConditionType>
auto operator|(const Read<ContainerType, WhereType, OrderByType, LimitType>& _r,

View File

@@ -24,6 +24,8 @@ std::string column_to_sql_definition(const dynamic::Column& _col) noexcept;
std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept;
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept;
std::vector<std::string> get_primary_keys(
const dynamic::CreateTable& _stmt) noexcept;
@@ -159,6 +161,25 @@ std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept {
return stream.str();
}
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept {
std::stringstream stream;
stream << "DELETE FROM ";
if (_stmt.table.schema) {
stream << wrap_in_quotes(*_stmt.table.schema) << ".";
}
stream << wrap_in_quotes(_stmt.table.name);
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
stream << ";";
return stream.str();
}
std::vector<std::string> get_primary_keys(
const dynamic::CreateTable& _stmt) noexcept {
using namespace std::ranges::views;
@@ -228,10 +249,16 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept {
using S = std::remove_cvref_t<decltype(_s)>;
if constexpr (std::is_same_v<S, dynamic::CreateTable>) {
return create_table_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::DeleteFrom>) {
return delete_from_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::Insert>) {
return insert_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::SelectFrom>) {
return select_from_to_sql(_s);
} else {
static_assert(rfl::always_false_v<S>, "Unsupported type.");
}

View File

@@ -20,6 +20,8 @@ std::string condition_to_sql_impl(const ConditionType& _condition) noexcept;
std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept;
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept;
std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept;
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept;
@@ -131,6 +133,25 @@ std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept {
return stream.str();
}
std::string delete_from_to_sql(const dynamic::DeleteFrom& _stmt) noexcept {
std::stringstream stream;
stream << "DELETE FROM ";
if (_stmt.table.schema) {
stream << "\"" << *_stmt.table.schema << "\".";
}
stream << "\"" << _stmt.table.name << "\"";
if (_stmt.where) {
stream << " WHERE " << condition_to_sql(*_stmt.where);
}
stream << ";";
return stream.str();
}
std::string insert_to_sql(const dynamic::Insert& _stmt) noexcept {
using namespace std::ranges::views;
@@ -215,10 +236,16 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept {
using S = std::remove_cvref_t<decltype(_s)>;
if constexpr (std::is_same_v<S, dynamic::CreateTable>) {
return create_table_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::DeleteFrom>) {
return delete_from_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::Insert>) {
return insert_to_sql(_s);
} else if constexpr (std::is_same_v<S, dynamic::SelectFrom>) {
return select_from_to_sql(_s);
} else {
static_assert(rfl::always_false_v<S>, "Unsupported type.");
}

View File

@@ -0,0 +1,25 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/postgres.hpp>
#include <sqlgen/transpilation/to_select_from.hpp>
namespace test_delete_from_dry {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(postgres, test_delete_from_dry) {
using namespace sqlgen;
const auto query = delete_from<TestTable> | where("field2"_c > 0);
const auto expected = R"(DELETE FROM "TestTable" WHERE "field2" > 0;)";
EXPECT_EQ(sqlgen::postgres::to_sql(query), expected);
}
} // namespace test_delete_from_dry

View File

@@ -0,0 +1,47 @@
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <sqlgen.hpp>
#include <sqlgen/sqlite.hpp>
#include <vector>
namespace test_delete_from {
struct Person {
sqlgen::PrimaryKey<uint32_t> id;
std::string first_name;
std::string last_name;
int age;
};
TEST(sqlite, test_delete_from) {
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;
const auto query = delete_from<Person> | where("first_name"_c == "Hugo");
query(conn).value();
const auto people2 = sqlgen::read<std::vector<Person>>(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}])";
EXPECT_EQ(rfl::json::write(people2), expected);
}
} // namespace test_delete_from