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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab

## Data Types and Validation

- [sqlgen::Dynamic](dynamic.md) - How to define custom SQL types not natively supported by sqlgen
- [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
Expand Down
176 changes: 176 additions & 0 deletions docs/dynamic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# `sqlgen::Dynamic`

`sqlgen::Dynamic` lets you define custom SQL types that aren't natively supported by sqlgen. It works by returning a `sqlgen::dynamic::types::Dynamic` with a database type name string that the transpiler passes directly to the target database.

## Usage

### Parser specialization for boost::uuids::uuid

In this example, we demonstrate how you can use boost::uuids::uuid to automatically generate primary keys. This is not officially supported by the sqlgen library,
but it is very easy to build something like this using `sqlgen::Dynamic`.

The first step is to specialize the `sqlgen::parsing::Parser` for `boost::uuids::uuid` and implement `read`, `write`, and `to_type`:

```cpp
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <sqlgen/dynamic/types.hpp>
#include <sqlgen/parsing/Parser.hpp>

namespace sqlgen::parsing {

template <>
struct Parser<boost::uuids::uuid> {
using Type = boost::uuids::uuid;

static Result<boost::uuids::uuid> read(
const std::optional<std::string>& _str) {
if (!_str) {
return error("boost::uuids::uuid cannot be NULL.");
}
return boost::lexical_cast<boost::uuids::uuid>(*_str);
}

static std::optional<std::string> write(
const boost::uuids::uuid& _u) {
return boost::uuids::to_string(_u);
}

static dynamic::Type to_type() noexcept {
return sqlgen::dynamic::types::Dynamic{"UUID"};
}
};

} // namespace sqlgen::parsing
```

### Using `boost::uuids::uuid` in structs

You can then automatically generate random UUIDs:

```cpp
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>

struct Person {
sqlgen::PrimaryKey<boost::uuids::uuid> id =
boost::uuids::uuid(boost::uuids::random_generator()());
std::string first_name;
std::string last_name;
int age;
};
```

This generates SQL schema with the custom UUID type name:

```sql
CREATE TABLE IF NOT EXISTS "Person"(
"id" UUID NOT NULL,
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL,
PRIMARY KEY("id")
);
```

### Working with UUIDs

Note that you do not have to assign the UUIDs - this is done automatically:

```cpp
// Create and insert records
std::vector<Person> people = {
Person{.first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{.first_name = "Marge", .last_name = "Simpson", .age = 42},
Person{.first_name = "Bart", .last_name = "Simpson", .age = 10},
Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8},
Person{.first_name = "Maggie", .last_name = "Simpson", .age = 0}
};

// Create table and write
auto res = conn.and_then(sqlgen::create_table<Person> | sqlgen::if_not_exists)
.and_then(sqlgen::insert(std::ref(people)));

// Read back ordered by age
using namespace sqlgen::literals;
const auto people2 = res.and_then(sqlgen::read<std::vector<Person>> |
sqlgen::order_by("age"_c.desc()))
.value();

// Filtering by UUID
const auto target = boost::lexical_cast<boost::uuids::uuid>(
"550e8400-e29b-41d4-a716-446655440000");
const auto query = sqlgen::read<std::vector<Person>> |
sqlgen::where("id"_c == target);
```

## Per-database type name for UUID

You may want to map `boost::uuids::uuid` to different database type names per dialect. Implement `to_type()` accordingly. The tests demonstrate these mappings:

- SQLite:
```cpp
static dynamic::Type to_type() noexcept {
return sqlgen::dynamic::types::Dynamic{"TEXT"};
}
```

- PostgreSQL:
```cpp
static dynamic::Type to_type() noexcept {
return sqlgen::dynamic::types::Dynamic{"UUID"};
}
```

- MySQL:
```cpp
static dynamic::Type to_type() noexcept {
return sqlgen::dynamic::types::Dynamic{"VARCHAR(36)"};
}
```

## Parser specialization requirements

Specializing `sqlgen::parsing::Parser<T>` requires three methods. These guidelines help ensure correctness and portability:

1) read
```cpp
static Result<T> read(const std::optional<std::string>& dbValue);
```
- Responsibility: Convert from DB string (or null) to `T`.
- Null handling: If your field cannot be null, return `error("... cannot be NULL.")` when `dbValue` is `std::nullopt`.
- Validation: Parse and validate strictly. Return a descriptive error for malformed input.
- Normalization: If your string form can vary (case, hyphens), normalize consistently so `write(read(x))` is stable.

2) write
```cpp
static std::optional<std::string> write(const T& value);
```
- Responsibility: Convert `T` to the string the DB expects.
- Nulls: Return `std::nullopt` only if you intend to write SQL NULL.
- Round-trip: Aim for `read(write(v)) == v` (modulo normalization).

3) to_type
```cpp
static dynamic::Type to_type() noexcept;
```
- Responsibility: Provide the DB type name via `sqlgen::dynamic::types::Dynamic{"TYPE_NAME"}`.
- Dialect mapping: Choose a valid name for your target DB (e.g., `UUID` on PostgreSQL, `VARCHAR(36)` on MySQL, `TEXT` on SQLite for UUIDs).
- Column properties: Constraints like primary key, unique, and nullability are typically controlled by field wrappers (`sqlgen::PrimaryKey`, `sqlgen::Unique`, `std::optional<T>`). If you are building fully dynamic schemas, you may also set properties on `Dynamic`.

Additional best practices:
- Error messages: Keep them clear and specific to aid debugging.
- Performance: Prefer lightweight conversions in `read`/`write`; avoid expensive allocations inside hot loops.
- Testing: Add round-trip tests (insert/read/compare) like the provided UUID tests across dialects.
- Consistency: Ensure the chosen DB type name matches any indexes, constraints, and length limits you rely on.

## Notes

- `sqlgen::dynamic::types::Dynamic` has:
- `type_name`: SQL type name string
- `properties`: column properties (nullable, unique, primary, auto_incr, foreign_key_reference)
- Works with all operations: `create_table`, `insert`, `select`, `update`, `delete`
- The type name is passed directly to the database; ensure it is valid for the target dialect
- Keep specializations in the `sqlgen::parsing` namespace

10 changes: 5 additions & 5 deletions include/sqlgen/dynamic/Type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
namespace sqlgen::dynamic {

using Type =
rfl::TaggedUnion<"type", types::Unknown, types::Boolean, types::Float32,
types::Float64, types::Int8, types::Int16, types::Int32,
types::Int64, types::UInt8, types::UInt16, types::UInt32,
types::UInt64, types::Text, types::Date, types::Timestamp,
types::TimestampWithTZ, types::VarChar, types::Dynamic>;
rfl::TaggedUnion<"type", types::Unknown, types::Boolean, types::Dynamic,
types::Float32, types::Float64, types::Int8, types::Int16,
types::Int32, types::Int64, types::UInt8, types::UInt16,
types::UInt32, types::UInt64, types::Text, types::Date,
types::Timestamp, types::TimestampWithTZ, types::VarChar>;

} // namespace sqlgen::dynamic

Expand Down
10 changes: 5 additions & 5 deletions include/sqlgen/dynamic/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ struct Boolean {
Properties properties;
};

struct Dynamic {
std::string type_name;
Properties properties;
};

struct Float32 {
Properties properties;
};
Expand Down Expand Up @@ -93,11 +98,6 @@ struct VarChar {
Properties properties;
};

struct Dynamic {
std::string type_name;
Properties properties;
};

} // namespace sqlgen::dynamic::types

#endif
8 changes: 6 additions & 2 deletions src/sqlgen/mysql/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ std::string cast_type_to_sql(const dynamic::Type& _type) noexcept {
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";

} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else if constexpr (std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
std::is_same_v<T, dynamic::types::Int32> ||
Expand Down Expand Up @@ -833,6 +836,9 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";

} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else if constexpr (std::is_same_v<T, dynamic::types::Int8>) {
return "TINYINT";

Expand Down Expand Up @@ -865,8 +871,6 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
} else if constexpr (std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "DATETIME";
} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else if constexpr (std::is_same_v<T, dynamic::types::Unknown>) {
return "TEXT";
Expand Down
6 changes: 3 additions & 3 deletions src/sqlgen/postgres/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,9 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
if constexpr (std::is_same_v<T, dynamic::types::Boolean>) {
return "BOOLEAN";

} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else if constexpr (std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
std::is_same_v<T, dynamic::types::UInt8> ||
Expand Down Expand Up @@ -751,9 +754,6 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
} else if constexpr (std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "TIMESTAMP WITH TIME ZONE";

} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else if constexpr (std::is_same_v<T, dynamic::types::Unknown>) {
return "TEXT";
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/sqlgen/sqlite/to_sql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ std::string to_sql_impl(const dynamic::Statement& _stmt) noexcept {
std::string type_to_sql(const dynamic::Type& _type) noexcept {
return _type.visit([](const auto _t) -> std::string {
using T = std::remove_cvref_t<decltype(_t)>;

if constexpr (std::is_same_v<T, dynamic::types::Boolean> ||
std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
Expand All @@ -730,18 +731,22 @@ std::string type_to_sql(const dynamic::Type& _type) noexcept {
std::is_same_v<T, dynamic::types::UInt32> ||
std::is_same_v<T, dynamic::types::UInt64>) {
return "INTEGER";

} else if constexpr (std::is_same_v<T, dynamic::types::Float32> ||
std::is_same_v<T, dynamic::types::Float64>) {
return "REAL";

} else if constexpr (std::is_same_v<T, dynamic::types::Unknown> ||
std::is_same_v<T, dynamic::types::Text> ||
std::is_same_v<T, dynamic::types::VarChar> ||
std::is_same_v<T, dynamic::types::Date> ||
std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "TEXT";

} else if constexpr (std::is_same_v<T, dynamic::types::Dynamic>) {
return _t.type_name;

} else {
static_assert(rfl::always_false_v<T>, "Not all cases were covered.");
}
Expand Down
Loading
Loading