📖 Documentation: Click here
sqlgen is a modern, type-safe ORM and SQL query generator for C++20, inspired by Python's SQLAlchemy/SQLModel and Rust's Diesel. It provides a fluent, composable interface for database operations with compile-time type checking and SQL injection protection.
sqlgen is based on and tightly integrated with reflect-cpp, a C++-20 library for fast serialization, deserialization and validation using reflection, similar to pydantic in Python, serde in Rust, encoding in Go or aeson in Haskell.
Together, reflect-cpp and sqlgen enable reliable and efficient ETL pipelines.
- 🔒 Type Safety: Compile-time validation of table schemas and queries
- 🛡️ SQL Injection Protection: Built-in input validation and parameterized queries
- 🔄 Composable Queries: Fluent interface for building complex queries
- 🚀 High Performance: Efficient batch operations and prepared statements
- 📦 Modern C++: Leverages C++20 features for a clean, expressive API
- 🔌 Multiple Backends: Support for PostgreSQL and SQLite
- 🔍 Reflection Integration: Seamless integration with reflect-cpp
The following table lists the databases currently supported by sqlgen and the underlying libraries used:
| Database | Library | Version | License | Remarks |
|---|---|---|---|---|
| DuckDB | duckdb | >= 1.4.2 | MIT | |
| MySQL/MariaDB | libmariadb | >= 3.4.5 | LGPL | |
| PostgreSQL | libpq | >= 16.4 | PostgreSQL | Will work for all libpq-compatible databases |
| sqlite | sqlite | >= 3.49.1 | Public Domain |
You can install the latest release of sqlgen using either vcpkg or Conan.
Alternatively, you can build sqlgen from source using vcpkg:
- Make sure you have the required dependencies installed (skip this step on Windows):
sudo apt-get install autoconf bison flex # Linux (Ubuntu, Debian, ...)
brew install autoconf bison flex # macOS- Set up vcpkg:
git submodule update --init
./vcpkg/bootstrap-vcpkg.sh # Linux, macOS
./vcpkg/bootstrap-vcpkg.bat # Windows- Build the library:
cmake -S . -B build -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 4 # gcc, clang
cmake --build build --config Release -j 4 # MSVCThis will build the static library. To build the shared library
add -DBUILD_SHARED_LIBS=ON -DVCPKG_TARGET_TRIPLET=... to the first line.
Run ./vcpkg/vcpkg help triplets to view all supported triplets.
Common triplets for shared libraries are x64-linux-dynamic,
arm64-osx-dynamic or x64-osx-dynamic.
Add -DSQLGEN_MYSQL=ON to support MySQL/MariaDB. Add -DSQLGEN_DUCKDB=ON to support DuckDB.
- Include in your CMake project:
find_package(sqlgenREQUIRED)
target_link_libraries(your_targetPRIVATEsqlgen::sqlgen)You can also build sqlgen from source using Conan:
- Install Conan (assuming you have Python and pipx installed):
pipx install conan
conan profile detectFor older versions of pip, you can also use pip instead of pipx.
- Build the library:
conan build . --build=missing -s compiler.cppstd=gnu20This will build the static library. To build the shared library,
add -o */*:shared=True.
Add -o sqlgen/*:with_mysql=True to support MySQL/MariaDB.
- Include in your CMake project:
find_package(sqlgenREQUIRED)
target_link_libraries(your_targetPRIVATEsqlgen::sqlgen)You can call conan inspect . to get an overview of the supported options.
#include<sqlgen/sqlite.hpp>
#include<iostream>structUser {
std::string name;
int age;
};
intmain() {
// Connect to SQLite databaseconstauto conn = sqlgen::sqlite::connect("test.db");
// Create and insert a userconstauto user = User{.name = "John", .age = 30};
sqlgen::write(conn, user);
// Read all usersconstauto users = sqlgen::read<std::vector<User>>(conn).value();
for (constauto& u : users) {
std::cout << u.name << " is " << u.age << " years old\n";
}
}#include<sqlgen/postgres.hpp>// PostgreSQL connectionconstauto credentials = sqlgen::postgres::Credentials{
.user = "username",
.password = "password",
.host = "localhost",
.dbname = "mydb",
.port = 5432
};
constauto conn = sqlgen::postgres::connect(credentials);
// SQLite connectionconstauto sqlite_conn = sqlgen::sqlite::connect("database.db");structPerson {
std::string first_name;
std::string last_name;
uint32_t age;
std::optional<std::string> email; // Nullable field
};constauto people = std::vector<Person>({
Person{.first_name = "Homer", .last_name = "Simpson", .age = 45},
Person{.first_name = "Marge", .last_name = "Simpson", .age = 42}
});
// Automatically creates table if it doesn't exist// (recall that the table is fully defined at compile time)constauto result = sqlgen::write(conn, people);
if (!result) {
std::cerr << "Error: " << result.error().what() << std::endl;
}Or:
...
// write(...) abstracts these steps away, but// if you prefer more granular control, you// can use sqlgen::insert.constauto result = begin_transaction(conn)
.and_then(create_table<Person> | if_not_exists)
.and_then(insert(std::ref(people)))
.and_then(commit);
...Generated SQL:
BEGIN TRANSACTION;
CREATETABLEIF NOT EXISTS "Person" (
"first_name"TEXTNOT NULL,
"last_name"TEXTNOT NULL,
"age"INTEGERNOT NULL,
"email"TEXT
);
INSERT INTO"Person" ("first_name", "last_name", "age", "email") VALUES (?, ?, ?, ?);
COMMIT;#include<rfl/json.hpp>
#include<sqlgen/postgres.hpp>usingnamespacesqlgen;usingnamespacesqlgen::literals;// Build a query for adults, ordered by ageconstauto query = read<std::vector<Person>> |
where("age"_c >= 18) |
order_by("age"_c.desc(), "last_name"_c) |
limit(10);
// Execute the queryconstauto result = query(conn);
if (result) {
// Print results as JSON
std::cout << rfl::json::write(*result) << std::endl;
} else {
std::cerr << "Error: " << result.error().what() << std::endl;
}Generated SQL:
SELECT"first_name", "last_name", "age", "email"FROM"Person"WHERE"age">=18ORDER BY"age"DESC, "last_name"LIMIT10;usingnamespacesqlgen;usingnamespacesqlgen::literals;structChildren {
std::string last_name;
int num_children;
int max_age;
int min_age;
int sum_age;
};
constauto get_children = select_from<Person>(
"last_name"_c,
count().as<"num_children">(),
max("age"_c).as<"max_age">(),
min("age"_c).as<"min_age">(),
sum("age"_c).as<"sum_age">(),
) | where("age"_c < 18) | group_by("last_name"_c) | to<std::vector<Children>>;
const std::vector<Children> children = get_children(conn).value();Generated SQL:
SELECT"last_name",
COUNT(*) as"num_children",
MAX("age") as"max_age",
MIN("age") as"min_age",
SUM("age") as"sum_age"FROM"Person"WHERE"age"<18GROUP BY"last_name";usingnamespacesqlgen;usingnamespacesqlgen::literals;structParentAndChild {
std::string last_name;
std::string first_name_parent;
std::string first_name_child;
double parent_age_at_birth;
};
constauto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t1 | as<"first_name_parent">,
"first_name"_t3 | as<"first_name_child">,
("age"_t1 - "age"_t3) | as<"parent_age_at_birth">) |
inner_join<Relationship, "t2">("id"_t1 == "parent_id"_t2) |
inner_join<Person, "t3">("id"_t3 == "child_id"_t2) |
order_by("id"_t1, "id"_t3) | to<std::vector<ParentAndChild>>;Generated SQL:
SELECT t1."last_name"AS"last_name", t1."first_name"AS"first_name_parent", t3."first_name"AS"first_name_child", t1."age"- t3."age"AS"parent_age_at_birth"FROM"Person" t1 INNER JOIN"Relationship" t2 ON t1."id"= t2."parent_id"INNER JOIN"Person" t3
ON t3."id"= t2."child_id"ORDER BY t1."id", t3."id"usingnamespacesqlgen;usingnamespacesqlgen::literals;structParentAndChild {
std::string last_name;
std::string first_name_parent;
std::string first_name_child;
double parent_age_at_birth;
};
// First, create a subqueryconstauto get_children =
select_from<Relationship, "t1">("parent_id"_t1 | as<"id">,
"first_name"_t2 | as<"first_name">,
"age"_t2 | as<"age">) |
inner_join<Person, "t2">("id"_t2 == "child_id"_t1);
// Then use it as a source for another queryconstauto get_people =
select_from<Person, "t1">(
"last_name"_t1 | as<"last_name">,
"first_name"_t1 | as<"first_name_parent">,
"first_name"_t2 | as<"first_name_child">,
("age"_t1 - "age"_t2) | as<"parent_age_at_birth">) |
inner_join<"t2">(
get_children, // Use the subquery as the source"id"_t1 == "id"_t2) | order_by("id"_t1, "id"_t2) | to<std::vector<ParentAndChild>>;Generated SQL:
SELECT t1."last_name"AS"last_name", t1."first_name"AS"first_name_parent", t2."first_name"AS"first_name_child", t1."age"- t2."age"AS"parent_age_at_birth"FROM"Person" t1 INNER JOIN (
SELECT t1."parent_id"AS"id", t2."first_name"AS"first_name", t2."age"AS"age"FROM"Relationship" t1 INNER JOIN"Person" t2
ON t2."id"= t1."child_id"
) t2 ON t1."id"= t2."id"ORDER BY t1."id", t2."id"Or:
usingnamespacesqlgen;usingnamespacesqlgen::literals;structParentAndChild {
std::string last_name;
std::string first_name_parent;
std::string first_name_child;
double parent_age_at_birth;
};
// First, create a subqueryconstauto get_parents = select_from<Person, "t1">(
"child_id"_t2 | as<"id">,
"first_name"_t1 | as<"first_name">,
"last_name"_t1 | as<"last_name">,
"age"_t1 | as<"age">
) | inner_join<Relationship, "t2">("id"_t1 == "parent_id"_t2);
// Then use it as a source for another queryconstauto get_people = select_from<"t1">(
get_parents, // Use the subquery as the source"last_name"_t1 | as<"last_name">,
"first_name"_t1 | as<"first_name_parent">,
"first_name"_t2 | as<"first_name_child">,
("age"_t1 - "age"_t2) | as<"parent_age_at_birth">) | inner_join<Person, "t2">("id"_t1 == "id"_t2) | order_by("id"_t1, "id"_t2) | to<std::vector<ParentAndChild>>;Generated SQL:
SELECT t1."last_name"AS"last_name", t1."first_name"AS"first_name_parent", t2."first_name"AS"first_name_child", (t1."age") - (t2."age") AS"parent_age_at_birth"FROM (
SELECT t2."child_id"AS"id", t1."first_name"AS"first_name", t1."last_name"AS"last_name", t1."age"AS"age"FROM"Person" t1 INNER JOIN"Relationship" t2
ON t1."id"= t2."parent_id"
) t1 INNER JOIN"Person" t2 ON t1."id"= t2."id"ORDER BY t1."id", t2."id"sqlgen provides comprehensive compile-time checks and runtime protection:
// Compile-time error: No such column "color"constauto query = read<std::vector<Person>> |
where("color"_c == "blue");
// Compile-time error: Cannot compare column "age" to a string constauto query = read<std::vector<Person>> |
where("age"_c == "Homer");
// Compile-time error: "age" must be aggregated or included in GROUP BY constauto query = select_from<Person>(
"last_name"_c,
"age"_c ) | group_by("last_name"_c);
// Compile-time error: Cannot add string and int constauto query = select_from<Person>(
"last_name"_c + "age"_c );
// Runtime protection against SQL injection
std::vector<Person> get_people(constauto& conn, const sqlgen::AlphaNumeric& first_name) {
usingnamespacesqlgen;return (read<std::vector<Person>> | where("first_name"_c == first_name))(conn).value();
}
// This will be rejectedget_people(conn, "Homer' OR '1'='1"); // SQL injection attemptFor detailed documentation, visit our documentation page.
We welcome constructive criticism, feature requests and contributions! Please open an issue or a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.