relx 0.1.0
A Modern C++23 Type-Safe SQL Query Builder
Loading...
Searching...
No Matches
relx Namespace Reference

relx database connection More...

Namespaces

namespace  connection
 
namespace  detail
 
namespace  literals
 
namespace  migrations
 
namespace  pgsql_async_wrapper
 
namespace  query
 
namespace  result
 
namespace  schema
 
namespace  type_checking
 Type compatibility utilities for column comparisons.
 

Classes

class  RelxException
 Base exception class for relx errors. More...
 

Functions

std::string format_error (const connection::ConnectionError &error)
 Format a ConnectionError for exception messages.
 
std::string format_error (const query::QueryError &error)
 Format a QueryError for exception messages.
 
std::string format_error (const result::ResultError &error)
 Format a ResultError for exception messages.
 
template<typename T , typename E >
T & value_or_throw (std::expected< T, E > &result, const std::string &context="", const std::source_location &location=std::source_location::current())
 Function to extract a value from an expected or throw on error.
 
template<typename T , typename E >
value_or_throw (std::expected< T, E > &&result, const std::string &context="", const std::source_location &location=std::source_location::current())
 Overload for rvalue std::expected.
 
template<typename E >
void throw_if_failed (const std::expected< void, E > &result, const std::string &context="", const std::source_location &location=std::source_location::current())
 Function to check and throw on error with no return value.
 

Detailed Description

relx database connection

relx - A type-safe SQL library

relx result processing

relx query builder

relx::migrations - Database Schema Migration Library

This is the main include file for relx database connection functionality. It provides abstract classes and concrete implementations for connecting to databases.

Example usage:

#include <relx/schema.hpp>
#include <relx/query.hpp>
#include <relx/results.hpp>
// Define a table
struct Users {
static constexpr auto table_name = "users";
relx::column<Users, "id", int> id;
relx::column<Users, "name", std::string> name;
relx::column<Users, "email", std::string> email;
relx::column<Users, "age", int> age;
};
// Define a DTO for results
struct UserDTO {
int id;
std::string name;
std::string email;
};
// Create a connection with connection parameters
.host = "localhost",
.port = 5432,
.dbname = "mydb",
.user = "postgres",
.password = "postgres"
};
auto conn = relx::PostgreSQLConnection(params);
// Connect to the database
auto connect_result = conn.connect();
if (!connect_result) {
std::println("Connection error: {}", connect_result.error().message);
return 1;
}
// Create table instance
Users u;
// Create a simple select query
auto query = relx::select(u.id, u.name, u.email)
.from(u)
.where(u.age > 18);
// Execute the query with automatic DTO mapping
auto result = conn.execute<UserDTO>(query);
if (!result) {
std::println("Query error: {}", result.error().message);
return 1;
}
// Process the results using the DTO
for (const auto& user : *result) {
std::println("{}: {} <{}>", user.id, user.name, user.email);
}
// Alternatively, use structured bindings
auto raw_result = conn.execute(query);
if (raw_result) {
for (const auto& [id, name, email] : raw_result->as<int, std::string, std::string>()) {
std::println("{}: {} <{}>", id, name, email);
}
}
// Disconnect
conn.disconnect();
PostgreSQL implementation of the Connection interface.
Represents a column in a database table.
Definition column.hpp:217
auto as(const Expr &expr, std::string alias)
Create an aliased column expression.
STL namespace.
Basic parameters for a PostgreSQL connection.
PRIMARY KEY constraint.
Definition column.hpp:19

This library provides automatic database migration generation by diffing table structures.

Core API (Most Users)

The main functions you'll need for 99% of use cases:

  • generate_migration(old_table, new_table, options) - Diff two table versions
  • generate_create_table_migration(table) - Create table migration
  • generate_drop_table_migration(table) - Drop table migration

Plus Migration (for .forward_sql()/.rollback_sql()) and MigrationOptions (for column mappings).

Advanced API (Power Users)

Available in relx::migrations::advanced:: namespace for:

  • Building custom migrations
  • Testing individual components
  • Introspecting migration operations
  • Extending the migration system

Example usage:

#include <relx/schema.hpp>
// Define your old table version
struct UsersV1 {
static constexpr auto table_name = "users";
relx::column<UsersV1, "id", int, relx::primary_key> id;
relx::column<UsersV1, "name", std::string> name;
relx::column<UsersV1, "email", std::string> email;
};
// Define your new table version
struct UsersV2 {
static constexpr auto table_name = "users";
relx::column<UsersV2, "id", int, relx::primary_key> id;
relx::column<UsersV2, "name", std::string> name;
relx::column<UsersV2, "email", std::string> email;
relx::column<UsersV2, "age", std::optional<int>> age; // New column
relx::column<UsersV2, "created_at", std::string,
relx::string_default<"CURRENT_TIMESTAMP", true>> created_at; // New column
};
int main() {
UsersV1 old_users;
UsersV2 new_users;
// Generate migration from V1 to V2
auto migration = relx::migrations::generate_migration(old_users, new_users);
// Get forward migration SQL
auto forward_sqls = migration.forward_sql();
for (const auto& sql : forward_sqls) {
std::println("Forward: {}", sql);
}
// Output:
// Forward: ALTER TABLE users ADD COLUMN age INTEGER;
// Forward: ALTER TABLE users ADD COLUMN created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP;
// Get rollback migration SQL
auto rollback_sqls = migration.rollback_sql();
for (const auto& sql : rollback_sqls) {
std::println("Rollback: {}", sql);
}
// Output:
// Rollback: ALTER TABLE users DROP COLUMN created_at;
// Rollback: ALTER TABLE users DROP COLUMN age;
// Check if migration has operations
if (!migration.empty()) {
std::println("Migration '{}' has {} operations", migration.name(), migration.size());
}
return 0;
}
Represents a UNIQUE constraint on a table.
MigrationResult< Migration > generate_migration(const OldTable &old_table, const NewTable &new_table, const MigrationOptions &options={})
Generate migration from old table to new table.
Definition diff.hpp:195
DEFAULT value for string literals.
Definition column.hpp:168

Column and Constraint Renaming

You can specify mappings to handle column renames and avoid data loss:

// Old table structure
struct EmployeesV1 {
static constexpr auto table_name = "employees";
relx::column<EmployeesV1, "first_name", std::string> first_name;
relx::column<EmployeesV1, "email_addr", std::string> email_addr;
};
// New table structure with renamed columns
struct EmployeesV2 {
static constexpr auto table_name = "employees";
relx::column<EmployeesV2, "given_name", std::string> given_name; // renamed
relx::column<EmployeesV2, "email", std::string> email; // renamed
};
EmployeesV1 old_table;
EmployeesV2 new_table;
// Without mappings - causes data loss (DROP + ADD)
auto migration_data_loss = relx::migrations::generate_migration(old_table, new_table);
// Generates: DROP first_name, DROP email_addr, ADD given_name, ADD email
// With mappings - preserves data (RENAME)
options.column_mappings = {
{"first_name", "given_name"},
{"email_addr", "email"}
};
auto migration_safe = relx::migrations::generate_migration(old_table, new_table, options);
auto safe_sqls = migration_safe.forward_sql();
// Output:
// ALTER TABLE employees RENAME COLUMN first_name TO given_name;
// ALTER TABLE employees RENAME COLUMN email_addr TO email;
auto safe_rollback = migration_safe.rollback_sql();
// Output (in reverse order):
// ALTER TABLE employees RENAME COLUMN email TO email_addr;
// ALTER TABLE employees RENAME COLUMN given_name TO first_name;
Options for controlling migration generation.
Definition diff.hpp:26
std::unordered_map< std::string, std::string > column_mappings
Map of old column name to new column name for renames.
Definition diff.hpp:28

Column Rename + Type Change

You can also handle cases where a column is both renamed and has its type changed:

struct ProductsV1 {
static constexpr auto table_name = "products";
relx::column<ProductsV1, "price_cents", int> price_cents; // int cents
};
struct ProductsV2 {
static constexpr auto table_name = "products";
relx::column<ProductsV2, "price_dollars", std::string> price_dollars; // string dollars
};
ProductsV1 old_products;
ProductsV2 new_products;
options.column_mappings = {{"price_cents", "price_dollars"}};
{"price_cents", {"CAST(price_cents / 100.0 AS TEXT)", "CAST(REPLACE(price_dollars, '$', '')
AS DECIMAL) * 100"}}
};
auto migration = relx::migrations::generate_migration(old_products, new_products, options);
auto forward_sqls = migration.forward_sql();
// Output:
// ALTER TABLE products ADD COLUMN price_dollars TEXT NOT NULL;
// UPDATE products SET price_dollars = CAST(price_cents / 100.0 AS TEXT);
// ALTER TABLE products DROP COLUMN price_cents;
auto rollback_sqls = migration.rollback_sql();
// Output (in reverse order):
// ALTER TABLE products ADD COLUMN price_cents INTEGER NOT NULL;
// UPDATE products SET price_cents = CAST(REPLACE(price_dollars, '$', '') AS DECIMAL) * 100;
// ALTER TABLE products DROP COLUMN price_dollars;
std::unordered_map< std::string, std::pair< std::string, std::string > > column_transformations
Bidirectional SQL transformations for complex column changes Key: old column name,...
Definition diff.hpp:40

You can also generate migrations for creating or dropping entire tables:

// Create a new table
struct NewTable {
static constexpr auto table_name = "new_table";
relx::column<NewTable, "id", int, relx::primary_key> id;
relx::column<NewTable, "data", std::string> data;
};
NewTable new_table;
// Generate CREATE TABLE migration
auto create_migration = relx::migrations::generate_create_table_migration(new_table);
auto create_sqls = create_migration.forward_sql();
// Forward: CREATE TABLE new_table (id INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY (id));
auto create_rollback_sqls = create_migration.rollback_sql();
// Rollback: DROP TABLE IF EXISTS new_table;
// Generate DROP TABLE migration
auto drop_migration = relx::migrations::generate_drop_table_migration(new_table);
auto drop_sqls = drop_migration.forward_sql();
// Forward: DROP TABLE IF EXISTS new_table;
auto drop_rollback_sqls = drop_migration.rollback_sql();
// Rollback: CREATE TABLE new_table (id INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY (id));
MigrationResult< Migration > generate_create_table_migration(const Table &table)
Generate migration to create a new table.
Definition diff.hpp:218
MigrationResult< Migration > generate_drop_table_migration(const Table &table)
Generate migration to drop a table.
Definition diff.hpp:229

This is the main include file for relx query building functionality. It provides a fluent interface for constructing type-safe SQL queries.

Example usage:

#include <relx/schema.hpp>
#include <relx/query.hpp>
// Define a table
struct Users {
static constexpr auto table_name = "users";
relx::column<Users, "id", int> id;
relx::column<Users, "name", std::string> name;
relx::column<Users, "email", std::string> email;
relx::column<Users, "age", int> age;
};
// Create table instance
Users u;
// Option 1: Simple select query with modern syntax
auto query1 = relx::select(u.id, u.name, u.email)
.from(u)
.where(u.age > 18);
// Option 2: Using explicit functions for clarity
auto query2 = relx::select(u.id, u.name, u.email)
.from(u)
.where(relx::to_expr(u.age) > relx::val(18));
// Option 3: Using SQL literal suffix
auto query3 = relx::select(u.id, u.name, u.email)
.from(u)
.where(u.age > 18_sql);
// Get the SQL and parameters
std::string sql = query1.to_sql(); // SELECT users.id, users.name, users.email FROM users WHERE
(users.age > ?) auto params = query1.bind_params(); // ["18"]
// Complex queries with joins
struct Posts {
static constexpr auto table_name = "posts";
relx::column<Posts, "id", int> id;
relx::column<Posts, "user_id", int> user_id;
relx::column<Posts, "title", std::string> title;
};
Posts p;
// Join query
auto join_query = relx::select(u.name, p.title)
.from(u)
.join(p, relx::on(u.id == p.user_id))
.where(u.age > 21)
.order_by(relx::desc(p.title));
// Aggregation functions
auto agg_query = relx::select_expr(
relx::count_all().as("user_count"),
relx::avg(u.age).as("average_age")
)
.from(u)
.where(u.age > 21);
// Update query
auto update_query = relx::update(u)
.set(
relx::set(u.name, "John Smith"),
relx::set(u.email, "john.smith@example.com")
)
.where(u.id == 1);
// Delete query
auto delete_query = relx::delete_from(u)
.where(u.age < 18);
// Insert query
auto insert_query = relx::insert_into(u)
.values(
relx::set(u.name, "Alice"),
relx::set(u.email, "alice@example.com"),
relx::set(u.age, 25)
);
Represents a foreign key constraint on a table.

This is the main include file for relx result processing functionality. It provides a way to parse and process query results in a type-safe manner.

Example usage:

#include <relx/schema.hpp>
#include <relx/query.hpp>
#include <relx/results.hpp>
// Define a table
struct Users {
static constexpr auto table_name = "users";
relx::column<Users, "id", int> id;
relx::column<Users, "name", std::string> name;
relx::column<Users, "email", std::string> email;
relx::column<Users, "age", int> age;
};
// Create connection
auto conn = relx::PostgreSQLConnection(params);
auto connect_result = conn.connect();
if (!connect_result) {
std::println("Connection error: {}", connect_result.error().message);
return 1;
}
// Create a DTO that matches query fields
struct UserDTO {
int id;
std::string name;
std::string email;
};
// Create table instance
Users u;
// Create a simple select query
auto query = relx::select(u.id, u.name, u.email)
.from(u)
.where(u.age > 18);
// Execute with automatic DTO mapping (preferred approach)
auto dto_result = conn.execute<UserDTO>(query);
if (dto_result) {
for (const auto& user : *dto_result) {
std::println("{}: {} <{}>", user.id, user.name, user.email);
}
}
// Execute and process the raw results
auto result = conn.execute(query);
if (!result) {
std::println("Query error: {}", result.error().message);
return 1;
}
// 1. Simple iteration with indexed access
for (const auto& row : *result) {
auto id = row.get<int>(0);
auto name = row.get<std::string>(1);
auto email = row.get<std::string>(2);
if (id && name && email) {
std::println("{}: {} <{}>", *id, *name, *email);
}
}
// 2. Using column names
for (const auto& row : *result) {
auto id = row.get<int>("id");
auto name = row.get<std::string>("name");
auto email = row.get<std::string>("email");
if (id && name && email) {
std::println("{}: {} <{}>", *id, *name, *email);
}
}
// 3. Using column objects
for (const auto& row : *result) {
auto id = row.get<int>(u.id);
auto name = row.get<std::string>(u.name);
auto email = row.get<std::string>(u.email);
if (id && name && email) {
std::println("{}: {} <{}>", *id, *name, *email);
}
}
// 4. Using structured binding (C++17)
for (const auto& [id, name, email] : result->as<int, std::string, std::string>()) {
std::println("{}: {} <{}>", id, name, email);
}
// 5. Transforming to custom objects
struct UserData {
int id;
std::string name;
std::string email;
};
auto users = result->transform<UserData>([](const auto& row) ->
relx::ResultProcessingResult<UserData> { auto id = row.get<int>("id"); auto name =
row.get<std::string>("name"); auto email = row.get<std::string>("email");
if (!id || !name || !email) {
return std::unexpected(relx::ResultError{"Error transforming row"});
}
return UserData{*id, *name, *email};
});
// Handle nullable columns with std::optional
auto optional_query = relx::select(u.id, u.name, u.email)
.from(u);
auto optional_result = conn.execute(optional_query);
if (optional_result) {
for (const auto& row : *optional_result) {
auto email = row.get<std::optional<std::string>>("email");
if (email && *email) {
// Email exists and is not null
std::println("Email: {}", **email);
} else if (email) {
// Email exists but is null
std::println("Email is NULL");
}
}
}
Error type for result processing operations.
Definition result.hpp:27

This file includes all the necessary headers to define database schemas.

Example usage:

#include <relx/schema.hpp>
// Define a table
struct Users {
static constexpr auto table_name = "users";
// Define columns
relx::column<Users, "id", int> id;
relx::column<Users, "name", std::string> name;
relx::column<Users, "email", std::string> email;
// Nullable column using std::optional
relx::column<Users, "bio", std::optional<std::string>> bio;
// Column with default value
relx::column<Users, "age", int, relx::default_value<18>> age;
// Default value for float
relx::column<Users, "score", double, relx::default_value<0.0>> score;
// Default value for boolean
relx::column<Users, "is_active", bool, relx::default_value<true>> is_active;
// Default value for string
relx::column<Users, "status", std::string, relx::string_default<"pending">> status;
// SQL literal default value
relx::column<Users, "created_at", std::string, relx::string_default<"CURRENT_TIMESTAMP",
true>> created_at;
// Nullable with explicit NULL default
relx::column<Users, "notes", std::optional<std::string>, relx::null_default> notes;
// Define a primary key
// Define a unique constraint
// Define a check constraint on a column
relx::table_check_constraint<&Users::age, ">= 18"> age_check;
// Define a table-level check constraint
relx::table_check_constraint<"status IN ('pending', 'active', 'suspended')"> status_check;
};
// Define another table with foreign key relationship
struct Posts {
static constexpr auto table_name = "posts";
relx::column<Posts, "id", int> id;
relx::column<Posts, "title", std::string> title;
relx::column<Posts, "content", std::string> content;
relx::column<Posts, "user_id", int> user_id;
// Define a primary key
// Define a foreign key relationship to Users table
// Define an index for faster lookups
};
// Generate the SQL for creating the tables
Users users;
Posts posts;
std::string users_sql = relx::create_table(users);
std::string posts_sql = relx::create_table(posts);
// Result for users:
// CREATE TABLE IF NOT EXISTS users (
// id INTEGER NOT NULL,
// name TEXT NOT NULL,
// email TEXT NOT NULL,
// bio TEXT,
// age INTEGER NOT NULL DEFAULT 18,
// score REAL NOT NULL DEFAULT 0.0,
// is_active BOOLEAN NOT NULL DEFAULT true,
// status TEXT NOT NULL DEFAULT 'pending',
// created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
// notes TEXT DEFAULT NULL,
// PRIMARY KEY (id),
// UNIQUE (email),
// CHECK (age >= 18),
// CHECK (status IN ('pending', 'active', 'suspended'))
// )
// Result for posts:
// CREATE TABLE IF NOT EXISTS posts (
// id INTEGER NOT NULL,
// title TEXT NOT NULL,
// content TEXT NOT NULL,
// user_id INTEGER NOT NULL,
// PRIMARY KEY (id),
// FOREIGN KEY (user_id) REFERENCES users (id)
// )
// CREATE INDEX posts_user_id_idx ON posts (user_id)
Generate CREATE TABLE SQL statement for a table struct.
Definition table.hpp:113
Represents an index on a table.
Definition index.hpp:39
Check constraint that accepts a condition string at compile time.
DEFAULT value for non-string values.
Definition column.hpp:147
NULL default for optional types.
Definition column.hpp:183

Function Documentation

◆ format_error() [1/3]

std::string relx::format_error ( const connection::ConnectionError error)
inline

Format a ConnectionError for exception messages.

Definition at line 29 of file error_handling.hpp.

◆ format_error() [2/3]

std::string relx::format_error ( const query::QueryError error)
inline

Format a QueryError for exception messages.

Definition at line 36 of file error_handling.hpp.

◆ format_error() [3/3]

std::string relx::format_error ( const result::ResultError error)
inline

Format a ResultError for exception messages.

Definition at line 43 of file error_handling.hpp.

◆ throw_if_failed()

template<typename E >
void relx::throw_if_failed ( const std::expected< void, E > &  result,
const std::string &  context = "",
const std::source_location &  location = std::source_location::current() 
)

Function to check and throw on error with no return value.

Definition at line 92 of file error_handling.hpp.

◆ value_or_throw() [1/2]

template<typename T , typename E >
T relx::value_or_throw ( std::expected< T, E > &&  result,
const std::string &  context = "",
const std::source_location &  location = std::source_location::current() 
)

Overload for rvalue std::expected.

Definition at line 75 of file error_handling.hpp.

◆ value_or_throw() [2/2]

template<typename T , typename E >
T & relx::value_or_throw ( std::expected< T, E > &  result,
const std::string &  context = "",
const std::source_location &  location = std::source_location::current() 
)

Function to extract a value from an expected or throw on error.

Template Parameters
TType of the value in std::expected
EType of the error in std::expected
Parameters
resultThe std::expected object to check
contextOptional context message to include in the exception
Returns
T& Reference to the contained value if no error
Exceptions
RelxExceptionif the expected contains an error

Definition at line 58 of file error_handling.hpp.