Getting Started

Reflect requires a compiler with C++26 static reflection support and the -freflection compiler flag.

Build

cmake -S . -B build -DREFLECT_BUILD_TESTS=ON -DREFLECT_BUILD_EXAMPLES=ON
cmake --build build
ctest --test-dir build --output-on-failure

Tests and examples are off by default so Reflect can be consumed by another CMake project without building development targets.

The SQLite backend is enabled by default and uses bundled SQLite through libsl3. The PostgreSQL backend is disabled by default, so the default build does not require PostgreSQL or libpq to be installed.

Consume With FetchContent

cmake_minimum_required(VERSION 3.30)

project(my_app LANGUAGES CXX)

include(FetchContent)

FetchContent_Declare(
    reflect
    GIT_REPOSITORY https://github.com/swordfatih/reflect.git
    GIT_TAG main
)

set(REFLECT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(REFLECT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(reflect)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE reflect::reflect)

To enable the PostgreSQL backend, set the option before FetchContent_MakeAvailable(reflect):

set(REFLECT_ENABLE_POSTGRESQL ON CACHE BOOL "" FORCE)

PostgreSQL support uses taocpp/taopq and requires the PostgreSQL client library and headers (libpq) to be installed where CMake can find them.

Reflect requires C++26 static reflection support. The reflect::reflect target adds the required -freflection compile option to consumers.

Connect

reflect::client sqlite{"sqlite://:memory:"};
reflect::client file_db{"sqlite://app.db"};
reflect::client postgres{"postgresql://user:pass@localhost/app"};

Supported URI prefixes are:

  • sqlite://
  • postgres://
  • postgresql://

SQLite accepts :memory: and file paths after the prefix. PostgreSQL forwards the connection string to the PostgreSQL backend when Reflect is built with REFLECT_ENABLE_POSTGRESQL=ON.

Create Or Sync A Table

db.migrate<User>();

migrate<T>() creates the table if missing, adds missing columns, creates indexes, and then validates the schema.

Validate On Startup

db.require_schema<User>();
db.require_schema<Post>();

Use validation as a startup guard before the application starts accepting writes.

Query

auto users = db.find_many<User>(
    reflect::query<User>(reflect::where(&User::email).contains("@example.test"))
        .order_by(&User::name)
        .take(50)
);

Transactions

db.transaction([](reflect::client& tx) {
    tx.insert<User>({.email = "ada@example.test", .name = "Ada"});
    tx.insert<User>({.email = "grace@example.test", .name = "Grace"});
});

Nested transactions use savepoints.

Inspect A Table

auto table = db.inspect<User>();

if(table.exists())
{
    for(const auto& column: table.columns)
    {
        // column.name, column.sql_type, column.nullable, column.primary_key
    }
}

Use inspect_table("table_name") when you do not have a model type.

Direct SQL

db.execute(reflect::statement{
    .sql = "CREATE INDEX IF NOT EXISTS \"idx_users_email\" ON \"users\" (\"email\")",
});

auto rows = db.query(reflect::statement{
    .sql = "SELECT \"email\" FROM \"users\" WHERE \"id\" = ?",
    .binds = {std::int64_t{1}},
});

Use direct SQL for backend-specific features, views, custom indexes, and manual migrations.


This site uses Just the Docs, a documentation theme for Jekyll.