Added documentation

This commit is contained in:
Dr. Patrick Urbanke
2025-05-07 04:18:10 +02:00
parent 831ca01d43
commit b73e2a512d
7 changed files with 438 additions and 48 deletions

View File

@@ -54,6 +54,7 @@ const auto conn = sqlgen::postgres::connect(credentials);
using namespace sqlgen;
// Query that returns the 100 youngest children.
// Columns are referred to using the _c operator.
const auto get_children = sqlgen::read<std::vector<People>> |
where("age"_c < 18) |
order_by("age"_c, "first_name"_c) |

View File

@@ -1,7 +1,12 @@
# Defining tables
In sqlgen, tables are defined using C++ structs. In its simplest
form, this can look like this:
`sqlgen` provides a type-safe way to define database tables using C++ structs. It automatically handles table creation, schema management, and nullability through C++ types.
## Usage
### Basic Table Definition
Define a table using a simple C++ struct:
```cpp
struct People {
@@ -11,80 +16,77 @@ struct People {
};
```
When you then try to write a vector of `People`,
the table will be automatically created, if necessary:
When you write data to this table, it will be automatically created if it doesn't exist:
```cpp
const auto people = std::vector<People>(...);
const auto result = sqlgen::write(conn, people);
```
In other words, `sqlgen::write(...)` might call the following SQL
statement under-the-hood:
This generates SQL similar to:
```sql
CREATE TABLE IF NOT EXISTS "People"("first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS "People"(
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL
);
```
(As with all SQL examples, the exact code will vary between dialects.)
### Custom Table Names
## Custom table names
Obviously, it is very convenient to just infer the table name
from the name of the struct. But sometimes we don't want that.
So we can do something like this:
Override the default table name using a static member:
```cpp
struct People {
constexpr static const char* tablename = "PEOPLE";
std::string first_name;
std::string last_name;
uint age;
};
```
Now, the generated SQL code will look like this:
This generates SQL with the custom table name:
```cpp
CREATE TABLE IF NOT EXISTS "PEOPLE"("first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL);
```sql
CREATE TABLE IF NOT EXISTS "PEOPLE"(
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL
);
```
## Custom schemata
### Custom Schemas
In a similar manner, you can embed your table in a schema:
Define a table within a specific schema:
```cpp
struct People {
constexpr static const char* schema = "my_schema";
std::string first_name;
std::string last_name;
uint age;
};
```
Now, the generated SQL code will look like this:
This generates SQL with the schema specification:
```cpp
CREATE TABLE IF NOT EXISTS "my_schema"."People"("first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL);
```sql
CREATE TABLE IF NOT EXISTS "my_schema"."People"(
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL
);
```
## Nullable types
### Nullable Fields
As we have seen, all columns are non-nullable by default. But what if we do want
nullability?
As with reflect-cpp, `std::optional`, `std::shared_ptr` and `std::unique_ptr` are interpreted
as nullable types. So you can create nullable fields as follows:
As with reflect-cpp, `std::optional`, `std::shared_ptr` and `std::unique_ptr` are interpreted as nullable types. So you can create nullable fields as follows:
```cpp
struct People {
@@ -94,5 +96,14 @@ struct People {
};
```
You can induce the NULL value by passing `std::nullopt` to age and NULL values
will be translated to `std::nullopt`.
## Notes
- All fields are non-nullable by default
- Nullable fields can be defined using:
- `std::optional<T>`
- `std::shared_ptr<T>`
- `std::unique_ptr<T>`
- Use `std::nullopt` or `nullptr` to represent NULL values
- The exact SQL generated may vary between database dialects
- Table names are inferred from struct names by default
- Custom table names and schemas can be specified using static members

View File

@@ -1,32 +1,45 @@
# `sqlgen::PrimaryKey`
# `sqlgen::PrimaryKey`
Sometimes you would like to set primary keys on your
tables. This is possible as well:
`sqlgen::PrimaryKey` is used to indicate which key should be a primary key.
## Usage
### Basic Definition
Define a primary key field in your struct:
```cpp
struct People {
rfl::PrimaryKey<std::string> first_name;
sqlgen::PrimaryKey<std::string> first_name;
std::string last_name;
uint age;
};
```
Now, the generated SQL code will look something like this:
This generates the following SQL schema:
```sql
CREATE TABLE IF NOT EXISTS "People"("first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL,
PRIMARY_KEY("first_name"));
CREATE TABLE IF NOT EXISTS "People"(
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"age" INTEGER NOT NULL,
PRIMARY_KEY("first_name")
);
```
`rfl::PrimaryKey<...>` is a simple wrapper. You can simply assign to it as follows:
### Assignment and Access
Assign values to primary key fields:
```cpp
const auto person = People{.first_name = "Homer", ...};
const auto person = People{
.first_name = "Homer",
.last_name = "Simpson",
.age = 45
};
```
And you can retrieve the underlying value using any of the following:
Access the underlying value using any of these methods:
```cpp
person.first_name();
@@ -34,3 +47,14 @@ person.first_name.get();
person.first_name.value();
```
## Notes
- The template parameter specifies the type of the primary key field
- Primary key fields are automatically marked as NOT NULL in the generated SQL
- The class supports:
- Direct value assignment
- Multiple access methods for the underlying value
- Reflection for SQL operations
- Move and copy semantics
- Primary keys can be used with any supported SQL data type

109
docs/reading.md Normal file
View File

@@ -0,0 +1,109 @@
# `sqlgen::read`
The `sqlgen::read` interface provides a flexible and type-safe way to query data from a SQL database into C++ containers or ranges. It supports composable query building with `where`, `order_by`, and `limit` clauses.
## Usage
### Basic Read
Read all rows from a table into a container (e.g., `std::vector<Person>`):
```cpp
const auto conn = sqlgen::sqlite::connect("database.db");
const auto people = sqlgen::read<std::vector<Person>>(conn).value();
```
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
const auto people = sqlgen::sqlite::connect("database.db").and_then(
sqlgen::read<std::vector<Person>>).value();
```
Please refer to the documentation on `sqlgen::Result<...>` for more information.
### With `where` clause
Filter results using a `where` clause:
```cpp
using namespace sqlgen;
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c < 18 and "first_name"_c != "Hugo");
const auto minors = query(conn).value();
```
Note that `"..."_c` refers to the name of the column. If such a field does not
exists 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 = sqlgen::read<std::vector<Person>> |
where("age"_c < 18 and "first_name"_c != "Hugo");
const auto minors = sqlite::connect("database.db").and_then(query).value();
```
### With `order_by` and `limit`
Sort and limit results:
```cpp
using namespace sqlgen;
const auto query = sqlgen::read<std::vector<Person>> |
order_by("age"_c) |
limit(2);
const auto youngest_two = query(conn).value();
```
### With ranges
Read results as a lazy range:
```cpp
const auto people_range = sqlgen::read<sqlgen::Range<Person>>(conn).value();
for (const sqlgen::Result<Person>& person : people_range) {
// process result
}
```
## Example: Full Query Composition
```cpp
using namespace sqlgen;
const auto query = sqlgen::read<std::vector<Person>> |
where("age"_c >= 18) |
order_by("last_name"_c, "first_name"_c.desc()) |
limit(10);
const auto adults = query(conn).value();
```
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::read<std::vector<Person>> |
sqlgen::where(sqlgen::col<"age"> >= 18) |
sqlgen::order_by(sqlgen::col<"last_name">, sqlgen::col<"first_name">.desc()) |
sqlgen::limit(10);
const auto adults = query(conn).value();
```
## Notes
- All query clauses (`where`, `order_by`, `limit`) are optional.
- The `Result<ContainerType>` type provides error handling; use `.value()` to extract the result (will throw a exception if the results) or handle errors as needed. Refer to the
- The `sqlgen::Range<T>` type allows for lazy iteration over results.
- `"..."_c` refers to the name of the column.

101
docs/result.md Normal file
View File

@@ -0,0 +1,101 @@
# `sqlgen::Result`
The `sqlgen::Result` type provides a way to handle errors without exceptions in sqlgen. It is an alias for `rfl::Result` and is used throughout sqlgen's API to handle potential failures in database operations.
Note that `sqlgen::Result` and `rfl::Result` are deliberately designed
to resemble C++ 23's `std::expected`.
## Usage
### Basic Usage
Handle database operations that might fail:
```cpp
// Reading from database
const auto people = sqlgen::read<std::vector<Person>>(conn);
if (people) {
// Success case - use the value
const auto& result = *people;
} else {
// Error case - handle the error
const auto& error = people.error();
}
// Writing to database
const auto write_result = sqlgen::write(conn, data);
if (!write_result) {
// Handle write error
const auto& error = write_result.error();
}
```
### Safe Value Access
Access values safely using different methods:
```cpp
const auto result = sqlgen::read<std::vector<Person>>(conn);
// Method 1: Using .value() (throws if error)
const auto person = result.value();
// Method 2: Using .value_or() (provides default if error)
const auto person = result.value_or(std::vector<Person>());
// Method 3: Using operator * (undefined if error)
if (result) {
const auto& person = *result;
}
```
### Monadic Operations
Chain operations using monadic functions:
```cpp
// Transform successful results - note that this
// is not a particularly efficient way of counting,
// but it highlights the point.
const auto count = sqlgen::read<std::vector<Person>>(conn)
.transform([](const auto& people) {
return people.size();
});
// Chain operations that might fail
const auto result = sqlgen::sqlite::connect("database.db")
.and_then(sqlgen::read<std::vector<Person>>);
```
### Error Handling
Handle errors explicitly:
```cpp
const auto result = sqlgen::read<std::vector<Person>>(conn);
// Check for errors
if (!result) {
const auto& error = result.error();
// Handle error...
}
// Transform errors
const auto better_error = result.transform_error([](const auto& e) {
return sqlgen::error("Database error: " + e.what());
});
```
## Notes
- `sqlgen::Result` is an alias for `rfl::Result`
- All database operations in sqlgen return `Result` types
- The type supports:
- Safe value access with `.value()` and `.value_or()`
- Monadic operations with `.transform()` and `.and_then()`
- Error handling with `.error()`
- Boolean checks for success/failure
- Move and copy semantics
- It's recommended to handle errors explicitly rather than using `.value()` which throws exceptions

85
docs/varchar.md Normal file
View File

@@ -0,0 +1,85 @@
# `sqlgen::Varchar`
The `sqlgen::Varchar` class provides a type-safe way to handle SQL VARCHAR fields in C++. It enforces length checks at runtime.
## Usage
### Basic Construction
Create a VARCHAR field with a specific maximum length:
```cpp
// Create a VARCHAR(50) field
sqlgen::Varchar<50> name;
// Initialize with a string
sqlgen::Varchar<50> name("John Doe");
// Initialize with a std::string
std::string str = "Jane Doe";
sqlgen::Varchar<50> name(str);
```
### Safe Construction with Result
Use the `make` function for safe construction with error handling:
```cpp
// Safe construction with error handling
auto result = sqlgen::Varchar<50>::make("John Doe");
if (result) {
auto name = result.value();
// Use name...
} else {
// Handle error...
}
```
### Assignment and Conversion
Assign values and convert between different VARCHAR sizes:
```cpp
struct Person{
sqlgen::Varchar<50> name;
};
person.name = "John Doe"; // Direct assignment
person.name = std::string("Jane Doe"); // From std::string
// Convert between different sizes
sqlgen::Varchar<100> long_name;
sqlgen::Varchar<50> short_name = long_name; // Will check size at runtime
```
### Accessing Values
Access the underlying string value:
```cpp
struct Person{
sqlgen::Varchar<50> name;
};
const auto person = Person{.name = "John Doe"};
// Get the value
const std::string& value1 = person.name();
const std::string& value2 = person.name.get();
const std::string& value3 = person.name.value();
// Get the maximum size
constexpr size_t max_size = name.size();
```
## Notes
- The template parameter `_size` specifies the maximum length of the VARCHAR field
- All string operations are checked against the maximum length
- The class provides both compile-time and runtime safety
- The `Result` type is used for safe construction and error handling
- The class supports:
- Direct string assignment
- Conversion between different VARCHAR sizes
- Reflection for SQL operations
- Move and copy semantics

59
docs/writing.md Normal file
View File

@@ -0,0 +1,59 @@
# `sqlgen::write`
The `sqlgen::write` interface provides a type-safe way to write data from C++ containers or ranges to a SQL database. It handles table creation, batch processing, and error handling automatically.
## Usage
### Basic Write
Write a container of objects to a database:
```cpp
const auto people = 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}
});
// Using with a connection reference
const auto conn = sqlgen::sqlite::connect();
sqlgen::write(conn, people);
```
### With Result<Ref<Connection>>
Handle connection creation and writing in a single chain:
```cpp
sqlgen::sqlite::connect("database.db")
.and_then([&](auto&& conn) { return sqlgen::write(conn, people); })
.value();
```
### With Iterators
Write a range of objects using iterators:
```cpp
std::vector<Person> people = /* ... */;
sqlgen::write(conn, people.begin(), people.end());
```
## How It Works
The `write` function performs the following operations in sequence:
1. Creates a table if it doesn't exist (using the object's structure)
2. Prepares an insert statement
3. Writes the data in batches of `SQLGEN_BATCH_SIZE`, which you can set at compile time
4. Handles any errors that occur during the process
## Notes
- The function automatically creates the table, if it doesn't exist
- Data is written in batches for better performance
- 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
- The function has three overloads:
1. Takes a connection reference and iterators
2. Takes a `Result<Ref<Connection>>` and iterators
3. Takes a connection and a container directly