diff --git a/README.md b/README.md index 1142a42..6432720 100644 --- a/README.md +++ b/README.md @@ -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> | where("age"_c < 18) | order_by("age"_c, "first_name"_c) | diff --git a/docs/defining_tables.md b/docs/defining_tables.md index 0028610..1105934 100644 --- a/docs/defining_tables.md +++ b/docs/defining_tables.md @@ -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(...); - 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` + - `std::shared_ptr` + - `std::unique_ptr` +- 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 diff --git a/docs/primary_key.md b/docs/primary_key.md index ec434f3..affd635 100644 --- a/docs/primary_key.md +++ b/docs/primary_key.md @@ -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 first_name; + sqlgen::PrimaryKey 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 + diff --git a/docs/reading.md b/docs/reading.md new file mode 100644 index 0000000..c6b7d20 --- /dev/null +++ b/docs/reading.md @@ -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`): + +```cpp +const auto conn = sqlgen::sqlite::connect("database.db"); + +const auto people = sqlgen::read>(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>).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> | + 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> | + 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> | + 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>(conn).value(); +for (const sqlgen::Result& person : people_range) { + // process result +} +``` + +## Example: Full Query Composition + +```cpp +using namespace sqlgen; + +const auto query = sqlgen::read> | + 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> | + 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` 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` type allows for lazy iteration over results. +- `"..."_c` refers to the name of the column. \ No newline at end of file diff --git a/docs/result.md b/docs/result.md new file mode 100644 index 0000000..df06d8e --- /dev/null +++ b/docs/result.md @@ -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>(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>(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()); + +// 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>(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>); +``` + +### Error Handling + +Handle errors explicitly: + +```cpp +const auto result = sqlgen::read>(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 diff --git a/docs/varchar.md b/docs/varchar.md new file mode 100644 index 0000000..3eea2eb --- /dev/null +++ b/docs/varchar.md @@ -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 diff --git a/docs/writing.md b/docs/writing.md new file mode 100644 index 0000000..a92b882 --- /dev/null +++ b/docs/writing.md @@ -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{.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> + +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 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` 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>` and iterators + 3. Takes a connection and a container directly +