mirror of
https://github.com/getml/sqlgen.git
synced 2025-12-31 06:30:18 -06:00
Added support for unique columns (#38)
This commit is contained in:
committed by
GitHub
parent
098deb9477
commit
ad1cae049b
@@ -42,6 +42,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab
|
||||
- [sqlgen::ForeignKey](foreign_key.md) - How to establish referential integrity between tables
|
||||
- [sqlgen::Pattern](pattern.md) - How to add regex pattern validation to avoid SQL injection
|
||||
- [sqlgen::Timestamp](timestamp.md) - How timestamps work in sqlgen
|
||||
- [sqlgen::Unique](unique.md) - How to enforce uniqueness constraints on table columns
|
||||
- [sqlgen::Varchar](varchar.md) - How varchars work in sqlgen
|
||||
|
||||
## Other concepts
|
||||
|
||||
145
docs/unique.md
Normal file
145
docs/unique.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# `sqlgen::Unique`
|
||||
|
||||
`sqlgen::Unique` is used to enforce uniqueness constraints on table columns. It ensures that no two rows in a table can have the same value for the specified column, providing data integrity at the database level.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Definition
|
||||
|
||||
Define a unique field in your struct by wrapping the type with `sqlgen::Unique`:
|
||||
|
||||
```cpp
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
std::string first_name;
|
||||
std::string last_name;
|
||||
sqlgen::Unique<double> age;
|
||||
};
|
||||
```
|
||||
|
||||
This generates the following SQL schema:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS "Person"(
|
||||
"id" INTEGER NOT NULL,
|
||||
"first_name" TEXT NOT NULL,
|
||||
"last_name" TEXT NOT NULL,
|
||||
"age" NUMERIC NOT NULL UNIQUE,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
```
|
||||
|
||||
### Template Parameters
|
||||
|
||||
The `sqlgen::Unique` template takes one parameter:
|
||||
|
||||
1. **T**: The type of the unique field (any supported SQL data type)
|
||||
|
||||
```cpp
|
||||
sqlgen::Unique<Type> field_name;
|
||||
```
|
||||
|
||||
### Type Safety and Validation
|
||||
|
||||
`sqlgen::Unique` provides compile-time validation and runtime uniqueness enforcement:
|
||||
|
||||
1. **Type Safety**: The wrapped type must be a supported SQL data type
|
||||
2. **Database-Level Enforcement**: The database enforces uniqueness constraints
|
||||
3. **Insert Validation**: You cannot insert duplicate values for unique fields
|
||||
|
||||
```cpp
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
sqlgen::Unique<std::string> first_name; // Each first_name must be unique
|
||||
std::string last_name;
|
||||
double age;
|
||||
};
|
||||
```
|
||||
|
||||
### Assignment and Access
|
||||
|
||||
Assign values to unique fields:
|
||||
|
||||
```cpp
|
||||
const auto person = Person{
|
||||
.id = 1,
|
||||
.first_name = "Homer", // Must be unique across all Person records
|
||||
.last_name = "Simpson",
|
||||
.age = 45
|
||||
};
|
||||
```
|
||||
|
||||
Access the underlying value using any of these methods:
|
||||
|
||||
```cpp
|
||||
person.first_name();
|
||||
person.first_name.get();
|
||||
person.first_name.value();
|
||||
```
|
||||
|
||||
### Working with Unique Constraints
|
||||
|
||||
Unique constraints are useful for ensuring data integrity in various scenarios:
|
||||
|
||||
```cpp
|
||||
struct User {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
sqlgen::Unique<std::string> email; // Each email must be unique
|
||||
sqlgen::Unique<std::string> username; // Each username must be unique
|
||||
std::string full_name;
|
||||
std::string password_hash;
|
||||
};
|
||||
|
||||
// Insert user records
|
||||
auto users = std::vector<User>({
|
||||
User{.id = 1, .email = "homer@simpson.com", .username = "homer", .full_name = "Homer Simpson", .password_hash = "hash1"},
|
||||
User{.id = 2, .email = "marge@simpson.com", .username = "marge", .full_name = "Marge Simpson", .password_hash = "hash2"}
|
||||
});
|
||||
|
||||
// Write to database - duplicate emails or usernames would cause an error
|
||||
conn.and_then(create_table<User> | if_not_exists)
|
||||
.and_then(insert(std::ref(users)));
|
||||
```
|
||||
|
||||
### Combining with Other Constraints
|
||||
|
||||
You can combine `sqlgen::Unique` with other constraints and types:
|
||||
|
||||
```cpp
|
||||
struct Employee {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
sqlgen::Unique<std::string> employee_id; // Unique employee ID
|
||||
sqlgen::Unique<std::string> email; // Unique email address
|
||||
std::string name;
|
||||
sqlgen::Unique<double> salary; // Unique salary (if applicable)
|
||||
};
|
||||
```
|
||||
|
||||
### Database-Specific Behavior
|
||||
|
||||
The generated SQL will be consistent across all supported databases:
|
||||
|
||||
```sql
|
||||
-- PostgreSQL
|
||||
CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER NOT NULL, "first_name" TEXT NOT NULL UNIQUE, "last_name" TEXT NOT NULL, "age" NUMERIC NOT NULL, PRIMARY KEY ("id"));
|
||||
|
||||
-- MySQL
|
||||
CREATE TABLE IF NOT EXISTS `Person` (`id` BIGINT NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `age` DECIMAL NOT NULL UNIQUE, PRIMARY KEY (`id`));
|
||||
|
||||
-- SQLite
|
||||
CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER PRIMARY KEY NOT NULL, "first_name" TEXT NOT NULL UNIQUE, "last_name" TEXT NOT NULL, "age" REAL NOT NULL);
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The template parameter specifies the type of the unique field
|
||||
- The class supports:
|
||||
- Direct value assignment
|
||||
- Multiple access methods for the underlying value
|
||||
- Reflection for SQL operations
|
||||
- Move and copy semantics
|
||||
- Compile-time type validation
|
||||
- Unique constraints can be used with any supported SQL data type
|
||||
- Database-level enforcement ensures data integrity across concurrent operations
|
||||
- Unique constraints are useful for business rules like unique emails, usernames, or SKU codes
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "sqlgen/Result.hpp"
|
||||
#include "sqlgen/Session.hpp"
|
||||
#include "sqlgen/Timestamp.hpp"
|
||||
#include "sqlgen/Unique.hpp"
|
||||
#include "sqlgen/Varchar.hpp"
|
||||
#include "sqlgen/aggregations.hpp"
|
||||
#include "sqlgen/as.hpp"
|
||||
|
||||
91
include/sqlgen/Unique.hpp
Normal file
91
include/sqlgen/Unique.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#ifndef SQLGEN_UNIQUE_HPP_
|
||||
#define SQLGEN_UNIQUE_HPP_
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Literal.hpp"
|
||||
#include "transpilation/Col.hpp"
|
||||
#include "transpilation/all_columns_exist.hpp"
|
||||
#include "transpilation/is_primary_key.hpp"
|
||||
#include "transpilation/remove_reflection_t.hpp"
|
||||
#include "transpilation/underlying_t.hpp"
|
||||
|
||||
namespace sqlgen {
|
||||
|
||||
template <class T>
|
||||
struct Unique {
|
||||
using ReflectionType = T;
|
||||
|
||||
Unique() : value_(T()) {}
|
||||
|
||||
Unique(const T& _value) : value_(_value) {}
|
||||
|
||||
Unique(Unique&& _other) noexcept = default;
|
||||
|
||||
Unique(const Unique& _other) = default;
|
||||
|
||||
template <class U,
|
||||
typename std::enable_if<std::is_convertible_v<U, ReflectionType>,
|
||||
bool>::type = true>
|
||||
Unique(const U& _value) : value_(_value) {}
|
||||
|
||||
template <class U,
|
||||
typename std::enable_if<std::is_convertible_v<U, ReflectionType>,
|
||||
bool>::type = true>
|
||||
Unique(U&& _value) noexcept : value_(std::forward<U>(_value)) {}
|
||||
|
||||
~Unique() = default;
|
||||
|
||||
/// Returns the underlying object.
|
||||
ReflectionType& get() { return value_; }
|
||||
|
||||
/// Returns the underlying object.
|
||||
const ReflectionType& get() const { return value_; }
|
||||
|
||||
/// Returns the underlying object.
|
||||
ReflectionType& operator()() { return value_; }
|
||||
|
||||
/// Returns the underlying object.
|
||||
const ReflectionType& operator()() const { return value_; }
|
||||
|
||||
/// Assigns the underlying object.
|
||||
auto& operator=(const ReflectionType& _value) {
|
||||
value_ = _value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Assigns the underlying object.
|
||||
template <class U,
|
||||
typename std::enable_if<std::is_convertible_v<U, ReflectionType>,
|
||||
bool>::type = true>
|
||||
auto& operator=(const U& _value) {
|
||||
value_ = _value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Assigns the underlying object.
|
||||
Unique& operator=(const Unique& _other) = default;
|
||||
|
||||
/// Assigns the underlying object.
|
||||
Unique& operator=(Unique&& _other) = default;
|
||||
|
||||
/// Necessary for the automated transpilation to work.
|
||||
const T& reflection() const { return value_; }
|
||||
|
||||
/// Assigns the underlying object.
|
||||
void set(const T& _value) { value_ = _value; }
|
||||
|
||||
/// Returns the underlying object.
|
||||
T& value() { return value_; }
|
||||
|
||||
/// Returns the underlying object.
|
||||
const T& value() const { return value_; }
|
||||
|
||||
/// The underlying value.
|
||||
T value_;
|
||||
};
|
||||
|
||||
} // namespace sqlgen
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@ struct Properties {
|
||||
bool auto_incr = false;
|
||||
bool primary = false;
|
||||
bool nullable = false;
|
||||
bool unique = false;
|
||||
std::optional<ForeignKeyReference> foreign_key_reference = std::nullopt;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Parser_shared_ptr.hpp"
|
||||
#include "Parser_string.hpp"
|
||||
#include "Parser_timestamp.hpp"
|
||||
#include "Parser_unique.hpp"
|
||||
#include "Parser_unique_ptr.hpp"
|
||||
#include "Parser_varchar.hpp"
|
||||
|
||||
|
||||
37
include/sqlgen/parsing/Parser_unique.hpp
Normal file
37
include/sqlgen/parsing/Parser_unique.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef SQLGEN_PARSING_PARSER_UNIQUE_HPP_
|
||||
#define SQLGEN_PARSING_PARSER_UNIQUE_HPP_
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#include "../Result.hpp"
|
||||
#include "../Unique.hpp"
|
||||
#include "../dynamic/Type.hpp"
|
||||
#include "Parser_base.hpp"
|
||||
|
||||
namespace sqlgen::parsing {
|
||||
|
||||
template <class T>
|
||||
struct Parser<Unique<T>> {
|
||||
static Result<Unique<T>> read(
|
||||
const std::optional<std::string>& _str) noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::read(_str).transform(
|
||||
[](auto&& _t) { return Unique<T>(std::move(_t)); });
|
||||
}
|
||||
|
||||
static std::optional<std::string> write(const Unique<T>& _f) noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::write(_f.value());
|
||||
}
|
||||
|
||||
static dynamic::Type to_type() noexcept {
|
||||
return Parser<std::remove_cvref_t<T>>::to_type().visit(
|
||||
[](auto _t) -> dynamic::Type {
|
||||
_t.properties.unique = true;
|
||||
return _t;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sqlgen::parsing
|
||||
|
||||
#endif
|
||||
@@ -689,13 +689,15 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept {
|
||||
}
|
||||
|
||||
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept {
|
||||
if (_p.auto_incr) {
|
||||
return " AUTO_INCREMENT";
|
||||
} else if (!_p.nullable) {
|
||||
return " NOT NULL";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return [&]() -> std::string {
|
||||
if (_p.auto_incr) {
|
||||
return " AUTO_INCREMENT";
|
||||
} else if (!_p.nullable) {
|
||||
return " NOT NULL";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}() + [&]() -> std::string { return _p.unique ? " UNIQUE" : ""; }();
|
||||
}
|
||||
|
||||
std::string select_from_to_sql(const dynamic::SelectFrom& _stmt) noexcept {
|
||||
|
||||
@@ -573,13 +573,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept {
|
||||
|
||||
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept {
|
||||
return [&]() -> std::string {
|
||||
if (_p.auto_incr) {
|
||||
return " GENERATED ALWAYS AS IDENTITY";
|
||||
} else if (!_p.nullable) {
|
||||
return " NOT NULL";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return std::string(_p.auto_incr ? " GENERATED ALWAYS AS IDENTITY" : "") +
|
||||
std::string(_p.nullable ? "" : " NOT NULL") +
|
||||
std::string(_p.unique ? " UNIQUE" : "");
|
||||
}() + [&]() -> std::string {
|
||||
if (!_p.foreign_key_reference) {
|
||||
return "";
|
||||
|
||||
@@ -582,7 +582,8 @@ std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept {
|
||||
return [&]() -> std::string {
|
||||
return std::string(_p.primary ? " PRIMARY KEY" : "") +
|
||||
std::string(_p.auto_incr ? " AUTOINCREMENT" : "") +
|
||||
std::string(_p.nullable ? "" : " NOT NULL");
|
||||
std::string(_p.nullable ? "" : " NOT NULL") +
|
||||
std::string(_p.unique ? " UNIQUE" : "");
|
||||
}() + [&]() -> std::string {
|
||||
if (!_p.foreign_key_reference) {
|
||||
return "";
|
||||
|
||||
54
tests/mysql/test_unique.cpp
Normal file
54
tests/mysql/test_unique.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/mysql.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_unique {
|
||||
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
std::string first_name;
|
||||
std::string last_name;
|
||||
sqlgen::Unique<double> age;
|
||||
};
|
||||
|
||||
TEST(mysql, test_unique) {
|
||||
const auto people = std::vector<Person>(
|
||||
{Person{
|
||||
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
|
||||
Person{
|
||||
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
|
||||
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
|
||||
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
|
||||
Person{
|
||||
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
|
||||
|
||||
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
|
||||
.user = "sqlgen",
|
||||
.password = "password",
|
||||
.dbname = "mysql"};
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
mysql::connect(credentials)
|
||||
.and_then(drop<Person> | if_exists)
|
||||
.and_then(create_table<Person>)
|
||||
.and_then(insert(std::ref(people)))
|
||||
.value();
|
||||
|
||||
const std::string expected_query =
|
||||
R"(CREATE TABLE IF NOT EXISTS `Person` (`id` BIGINT NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `age` DECIMAL NOT NULL UNIQUE, PRIMARY KEY (`id`));)";
|
||||
|
||||
EXPECT_EQ(mysql::to_sql(create_table<Person>), expected_query);
|
||||
}
|
||||
|
||||
} // namespace test_unique
|
||||
|
||||
#endif
|
||||
54
tests/postgres/test_unique.cpp
Normal file
54
tests/postgres/test_unique.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/postgres.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_unique {
|
||||
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
sqlgen::Unique<std::string> first_name;
|
||||
std::string last_name;
|
||||
double age;
|
||||
};
|
||||
|
||||
TEST(postgres, test_unique) {
|
||||
const auto people = std::vector<Person>(
|
||||
{Person{
|
||||
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
|
||||
Person{
|
||||
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
|
||||
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
|
||||
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
|
||||
Person{
|
||||
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
|
||||
|
||||
const auto credentials = sqlgen::postgres::Credentials{.user = "postgres",
|
||||
.password = "password",
|
||||
.host = "localhost",
|
||||
.dbname = "postgres"};
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
postgres::connect(credentials)
|
||||
.and_then(drop<Person> | if_exists)
|
||||
.and_then(create_table<Person>)
|
||||
.and_then(insert(std::ref(people)))
|
||||
.value();
|
||||
|
||||
const std::string expected_query =
|
||||
R"(CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER NOT NULL, "first_name" TEXT NOT NULL UNIQUE, "last_name" TEXT NOT NULL, "age" NUMERIC NOT NULL, PRIMARY KEY ("id"));)";
|
||||
|
||||
EXPECT_EQ(postgres::to_sql(create_table<Person>), expected_query);
|
||||
}
|
||||
|
||||
} // namespace test_unique
|
||||
|
||||
#endif
|
||||
46
tests/sqlite/test_unique.cpp
Normal file
46
tests/sqlite/test_unique.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/sqlite.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_unique {
|
||||
|
||||
struct Person {
|
||||
sqlgen::PrimaryKey<uint32_t> id;
|
||||
sqlgen::Unique<std::string> first_name;
|
||||
std::string last_name;
|
||||
double age;
|
||||
};
|
||||
|
||||
TEST(postgres, test_unique) {
|
||||
const auto people = std::vector<Person>(
|
||||
{Person{
|
||||
.id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45},
|
||||
Person{
|
||||
.id = 1, .first_name = "Marge", .last_name = "Simpson", .age = 40},
|
||||
Person{.id = 2, .first_name = "Bart", .last_name = "Simpson", .age = 10},
|
||||
Person{.id = 3, .first_name = "Lisa", .last_name = "Simpson", .age = 8},
|
||||
Person{
|
||||
.id = 4, .first_name = "Maggie", .last_name = "Simpson", .age = 0}});
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
sqlite::connect()
|
||||
.and_then(drop<Person> | if_exists)
|
||||
.and_then(create_table<Person>)
|
||||
.and_then(insert(std::ref(people)))
|
||||
.value();
|
||||
|
||||
const std::string expected_query =
|
||||
R"(CREATE TABLE IF NOT EXISTS "Person" ("id" INTEGER PRIMARY KEY NOT NULL, "first_name" TEXT NOT NULL UNIQUE, "last_name" TEXT NOT NULL, "age" REAL NOT NULL);)";
|
||||
|
||||
EXPECT_EQ(sqlite::to_sql(create_table<Person>), expected_query);
|
||||
}
|
||||
|
||||
} // namespace test_unique
|
||||
|
||||
Reference in New Issue
Block a user