mirror of
https://github.com/getml/sqlgen.git
synced 2026-02-16 05:38:33 -06:00
Added documentation
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
109
docs/reading.md
Normal 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
101
docs/result.md
Normal 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
85
docs/varchar.md
Normal 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
59
docs/writing.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user