This is a sophisticated, "bulletproof" architecture. By treating your database schema versions as immutable snapshots and forcing the generators to run as a deliberate manual step, you’re eliminating the "it worked on my machine" drift that plagues C++ development.
The following summary is designed to be pasted into Cursor's composer or chat. It provides the exact technical constraints and folder logic the AI needs to write modular, high-quality CMake files without making "lucky guesses" about your intent.
CppBeSSOT is a modular system for managing a C++ backend SSOT. It uses OpenAPI YAML files (one per table) as the source to drive C++ ORM code (ODB), JSON serialization, TypeScript types, and Zod validation schemas.
- Backend: C++ (ODB ORM + JSON library).
- Preferred JSON Serdes Provider:
nlohmann::json. - JSON Serdes Placement: Generated model classes should expose JSON serde in class definitions as class methods (not only external helpers).
- Frontend: TypeScript + Zod.
- Migrations: Automated SQL diffs generated by ODB's Schema Evolution engine.
- Integration: Designed as a git submodule to be included in larger projects.
Dependencies (C++): Generated C++ models depend on the nlohmann/json header-only library for JSON serialization (toJson() / fromJson() and the NLOHMANN_DEFINE_TYPE_INTRUSIVE macro). The build must have access to nlohmann headers (e.g. package nlohmann-json3-dev on Debian/Ubuntu). Treat this as a build-time (and runtime, if you ship the generated code) dependency; the dependency check or project README should document it.
${CPPBESSOT_WORKDIR}: Configurable via CMake (default:db). Contains the versioned data.- Submodule Path: The CMake logic resides in a submodule (e.g.,
cmake/cppbessot/). - Folder Structure:
${CPPBESSOT_WORKDIR}/
├── v<N>/ # Immutable version snapshots
│ ├── openapi/ # Source YAMLs
│ ├── generated-ts-types/ # Output: TS interfaces
│ ├── generated-zod/ # Output: Zod TS schemas
│ ├── generated-cpp-source/ # Output: C++ Headers (with ODB & JSON macros)
│ ├── generated-sql-ddl/ # Output: ODB-generated SQL DDL snapshots for this schema version
│ │ ├── sqlite/ # SQLite DDL output for this version
│ │ └── postgre/ # PostgreSQL DDL output for this version
│ └── generated-odb-source/ # Output: ODB compiler artifacts (+ per-backend changelog .xml)
│ ├── sqlite/ # SQLite ORM + changelog (e.g. Agent.xml, DeliveryRequest.xml)
│ └── postgre/ # PostgreSQL ORM + changelog
└── migrations/ # Version-to-version SQL diffs
└── v<a>-v<b>/
├── sqlite/
└── postgre/
Schema version numbering: CppBeSSOT schema version folders (e.g. v1.1, v1.2) must start at 1.x. ODB’s schema evolution uses #pragma db model version(base, current) for migration diffing, and both base and current must be non-zero positive integers. Using 0.x (e.g. v0.0, v0.1) would require a zero version in the pragma, which ODB rejects. Therefore the first schema version is v1.1, the next v1.2, and so on.
All CMake files are located in the submodule directory. The entry point is CppBeSSOT.cmake.
CppBeSSOT.cmake: The master include file.dbDependencyCheck.cmake: Checks forodb,npx,java,git,openapi-zod-client, and availability of nlohmann/json headers (e.g. include path or package). Fails if missing.dbGenerationCommon.cmake: Internal utility functions (path resolution, version validation).- Individual Logic Files:
dbGenTS.cmake,dbGenZod.cmake,dbGenCpp.cmake,dbGenODB.cmake,dbGenSqlDDL.cmake.
- Action: Runs
git status --porcelain ${CPPBESSOT_WORKDIR}/. - Behavior: If changes are detected in git-tracked files within the workdir, it issues a
WARNINGorSEND_ERRORadvising the dev to create a new version folder and re-run generation.
- Variable:
-DDB_SCHEMA_DIR_TO_GENERATE="v1" - Sub-targets (One for each
generated-*folder):
db_gen_ts:openapi-generator-cli-> TS types.db_gen_zod:openapi-zod-client --export-schemas-> Zod schemas.db_gen_cpp_headers:openapi-generator-cli+ Mustache templates -> C++ headers with ODB#pragmaand JSON macros. Use models-only mode (--global-property models): with nlohmann/json the generated model headers are self-contained (noModelBase.hor cpp-restsdk support files needed).db_gen_odb_logic:odbcompiler (with--changelog-dirto the same backend subdir under generated-odb-source) -> generated-odb-source.db_gen_sql_ddl:odb --generate-schema --schema-format sqlwith--changelog-dirpointing at the corresponding generated-odb-source backend subdir -> generated-sql-ddl (sqlite/postgre DDL snapshots); changelog XML remains in generated-odb-source.
- Variables:
-DDB_SCHEMA_DIR_MIGRATION_FROM="v1"and-DDB_SCHEMA_DIR_MIGRATION_TO="v2" - Action: Uses ODB's Schema Evolution engine to compare headers in
FROMvsTO. - Output: SQL migration scripts for both SQLite and PostgreSQL.
- Variable:
-DDB_SCHEMA_DIR_TO_GENERATE="v1" - Action: Runs ODB schema generation for each supported backend for the selected version.
- Output: Backend-specific SQL DDL under
${CPPBESSOT_WORKDIR}/v1/generated-sql-ddl/(for examplesqlite/andpostgre/).
- No Absolute Paths: All paths must be relative to
PROJECT_SOURCE_DIRor calculated viaCMAKE_CURRENT_LIST_DIR. - Persistence: Generated files are Git-tracked and must not be deleted by
make clean. - Manual Trigger: Targets should be
EXCLUDE_FROM_ALL. - Submodule Awareness: The CMake files will be in a subdirectory of the
bigproject. EnsureCppBeSSOT.cmakecorrectly includes its sibling.cmakefiles. - Generated Library Names:
- Build a library from
db/v<N>/generated-cpp-source/namedlibcppBeSsotOpenAiModelGen(static or shared is allowed). - Build backend-specific libraries from
db/v<N>/generated-odb-source/named:libcppBeSsotOdbSqlitelibcppBeSsotOdbPgSql
- Treat ODB backend libs as likely shared objects.
- Build a library from
- Prompt/Instruction File for OpenAPI Authors: In the final generated project, add an instruction file for LLM agents that create or update OpenAPI schema files. The instruction file must state:
- Use
lowerCamelCasefor property names. - Use
UpperCamelCasefor class/schema names. - Do not use underscores in class names or property names.
- Use
- No Version in C++ Namespace: Generated C++ model namespaces must not encode schema version names. The wider project should not need version-coupled source code identifiers; schema version selection should be done via include path selection (for example,
include_directoriespointing at the desired generated version). - Template Bootstrap Input: Use the current checked-in Mustache templates under
${CPPBESSOT_MODULE_ROOT}/openapi/templates/as initial test inputs when bringing up generation and CMake flows. Treat these templates as the baseline that should work first before further refactoring.
For ODB’s schema evolution to produce migration diffs between two schema versions, the following annotations are required.
In each OpenAPI schema YAML (per-table, under openapi/schema/):
x-odbModelVersion: "M, N"— Model version for this schema (e.g."1, 1"for v1.1,"1, 2"for v1.2). Used only for documentation; the Mustache template must emit the actual ODB pragma with non-zero integers (e.g.(1, 1)for first version,(1, 2)for next).x-odbTable: TableName— Exact table name for#pragma db object table("...")(e.g.Agent,DeliveryRequest).x-odbId: true— On the primary-key property (e.g.id) so the template can emit#pragma db idfor that member.x-odbAddedIn: "M.N"— On every persistent property, indicating the schema version where the column was added (e.g."1.0","1.1"). Enables correct versioning of new columns in migrations.
In the Mustache model-header template:
#include <odb/core.hxx>— So ODB sees the pragmas.#pragma db model version(base, current)— One per model;baseandcurrentmust be non-zero. Typically the first schema version uses(1, 1)and the next(1, 2). The template may hardcode these per version or derive fromx-odbModelVersion(mapping 1.0→1,1 and 1.1→1,2).#pragma db object table("...")— Fromx-odbTable(orclassnamefallback).#pragma db id— Placed immediately before the member that hasx-odbId: true.
Without these, ODB will not generate schema DDL or migration diffs correctly.
ODB writes schema evolution changelog files (e.g. Agent.xml, DeliveryRequest.xml) next to the input .h files by default; --output-dir does not change that. Changelog files are database-specific, so if you run SQLite then PostgreSQL (or vice versa) without redirecting them, the second run sees the first backend’s changelog and can fail with "wrong database" errors.
Use --changelog-dir so changelog is read from and written to the same backend-specific directory as the ORM output:
- For ORM (
odb ... -q -o .../generated-odb-source/sqlite): add--changelog-dir .../generated-odb-source/sqliteso any changelog lives undergenerated-odb-source/sqlite/. - For SQL DDL (
odb ... --generate-schema --schema-format sql -o .../generated-sql-ddl/sqlite): add--changelog-dir .../generated-odb-source/sqliteso the changelog stays ingenerated-odb-source(not next to the headers or ingenerated-sql-ddl). Same forpostgrewithgenerated-odb-source/postgreandgenerated-sql-ddl/postgre.
Result: each backend has its own generated-odb-source/<backend>/*.xml; you can run SQLite and PostgreSQL in any order without deleting changelog between runs. CMake/scripts should pass --changelog-dir on every odb invocation (ORM and DDL) for each backend.
Run all commands from the project repository root. Prerequisites: npx, odb compiler, and nlohmann/json headers. The current proof-of-concept uses schema versions v1.1 (first) and v1.2 (second, adds e.g. booyah to Agent).
# 1. C++ model headers (models-only: no CMake/ModelBase; nlohmann makes headers self-contained)
npx @openapitools/openapi-generator-cli generate -i db/v1.1/openapi/openapi.yaml -g cpp-restsdk -t openapi/templates/cpp-odb-json -c openapi/templates/cpp-odb-json/config.yaml -o db/v1.1/generated-cpp-source --global-property models
# 2. Create ODB and DDL output dirs
mkdir -p db/v1.1/generated-odb-source/sqlite db/v1.1/generated-odb-source/postgre \
db/v1.1/generated-sql-ddl/sqlite db/v1.1/generated-sql-ddl/postgre
# 3. ODB ORM (changelog goes to generated-odb-source/<backend>)
odb -I db/v1.1/generated-cpp-source/include --std c++11 -d sqlite -q \
-o db/v1.1/generated-odb-source/sqlite --changelog-dir db/v1.1/generated-odb-source/sqlite \
db/v1.1/generated-cpp-source/include/*/model/Agent.h db/v1.1/generated-cpp-source/include/*/model/DeliveryRequest.h
odb -I db/v1.1/generated-cpp-source/include --std c++11 -d pgsql -q \
-o db/v1.1/generated-odb-source/postgre --changelog-dir db/v1.1/generated-odb-source/postgre \
db/v1.1/generated-cpp-source/include/*/model/Agent.h db/v1.1/generated-cpp-source/include/*/model/DeliveryRequest.h
# 4. SQL DDL
odb -I db/v1.1/generated-cpp-source/include --std c++11 -d sqlite --generate-schema --schema-format sql -q \
-o db/v1.1/generated-sql-ddl/sqlite --changelog-dir db/v1.1/generated-odb-source/sqlite \
db/v1.1/generated-cpp-source/include/*/model/Agent.h db/v1.1/generated-cpp-source/include/*/model/DeliveryRequest.h
odb -I db/v1.1/generated-cpp-source/include --std c++11 -d pgsql --generate-schema --schema-format sql -q \
-o db/v1.1/generated-sql-ddl/postgre --changelog-dir db/v1.1/generated-odb-source/postgre \
db/v1.1/generated-cpp-source/include/*/model/Agent.h db/v1.1/generated-cpp-source/include/*/model/DeliveryRequest.h# 1. C++ model headers (models-only)
npx @openapitools/openapi-generator-cli generate -i db/v1.2/openapi/openapi.yaml -g cpp-restsdk -t openapi/templates/cpp-odb-json -c openapi/templates/cpp-odb-json/config.yaml -o db/v1.2/generated-cpp-source --global-property models
# 2. Create ODB and DDL output dirs
mkdir -p db/v1.2/generated-odb-source/sqlite db/v1.2/generated-odb-source/postgre \
db/v1.2/generated-sql-ddl/sqlite db/v1.2/generated-sql-ddl/postgre
# 3. ODB ORM
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d sqlite -q \
-o db/v1.2/generated-odb-source/sqlite --changelog-dir db/v1.2/generated-odb-source/sqlite \
db/v1.2/generated-cpp-source/include/*/model/Agent.h db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.h
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d pgsql -q \
-o db/v1.2/generated-odb-source/postgre --changelog-dir db/v1.2/generated-odb-source/postgre \
db/v1.2/generated-cpp-source/include/*/model/Agent.h db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.h
# 4. SQL DDL
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d sqlite --generate-schema --schema-format sql -q \
-o db/v1.2/generated-sql-ddl/sqlite --changelog-dir db/v1.2/generated-odb-source/sqlite \
db/v1.2/generated-cpp-source/include/*/model/Agent.h db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.h
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d pgsql --generate-schema --schema-format sql -q \
-o db/v1.2/generated-sql-ddl/postgre --changelog-dir db/v1.2/generated-odb-source/postgre \
db/v1.2/generated-cpp-source/include/*/model/Agent.h db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.hUses v1.1 changelog XML as input and v1.2 headers; writes migration SQL to db/migrations/v1.1-v1.2/<backend>/. One ODB invocation per model per backend (ODB requires one --changelog-in / --changelog-out pair per input header).
mkdir -p db/migrations/v1.1-v1.2/sqlite db/migrations/v1.1-v1.2/postgre
# SQLite
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d sqlite --generate-schema --schema-format sql -q \
-o db/migrations/v1.1-v1.2/sqlite \
--changelog-in db/v1.1/generated-odb-source/sqlite/Agent.xml \
--changelog-out db/v1.2/generated-odb-source/sqlite/Agent.xml \
db/v1.2/generated-cpp-source/include/*/model/Agent.h
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d sqlite --generate-schema --schema-format sql -q \
-o db/migrations/v1.1-v1.2/sqlite \
--changelog-in db/v1.1/generated-odb-source/sqlite/DeliveryRequest.xml \
--changelog-out db/v1.2/generated-odb-source/sqlite/DeliveryRequest.xml \
db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.h
# PostgreSQL
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d pgsql --generate-schema --schema-format sql -q \
-o db/migrations/v1.1-v1.2/postgre \
--changelog-in db/v1.1/generated-odb-source/postgre/Agent.xml \
--changelog-out db/v1.2/generated-odb-source/postgre/Agent.xml \
db/v1.2/generated-cpp-source/include/*/model/Agent.h
odb -I db/v1.2/generated-cpp-source/include --std c++11 -d pgsql --generate-schema --schema-format sql -q \
-o db/migrations/v1.1-v1.2/postgre \
--changelog-in db/v1.1/generated-odb-source/postgre/DeliveryRequest.xml \
--changelog-out db/v1.2/generated-odb-source/postgre/DeliveryRequest.xml \
db/v1.2/generated-cpp-source/include/*/model/DeliveryRequest.h"Based on the CppBeSSOT Design Spec, please create the entry-point
cmake/cppbessot/CppBeSSOT.cmakeand thecmake/cppbessot/dbDependencyCheck.cmakefile. Ensure the dependency check specifically looks for theodbcompiler andnpx, and provide clear error messages for Ubuntu users if they are missing."
Would you like me to draft a sample dbGenerationCommon.cmake that handles the boilerplate of resolving the folder paths for a specific version?
Prompt that generated this output from Gemini:
Yea: so I've decided my tech stack for SSOT, JSON serdes, ORM and DB migrations will be ODB + a C++ Json lib. The migration diffs are more powerful and maintainable; the cost of just adding a line to the openapi template is much lower than the cost of potentially having to deal with [semi-]manual migrations. Ok. Write up a summary of the design I'm trying to get done here, in a format that is accurate enough to immediately inform Cursor of what I'm trying to build, and therefore able to help Cursor to generate the build system commands for CMake that are required to construct the chain of operations from the OpenAPI SSOT to the end result of the generated ODB headers, JSON serdes support, migrations, ODB ORM, and Zod schema enforcement.
The ideal thing is that I have a folder structure like this: /project
- db/ # DB related stuff.
* v/ # Each version of the DB is saved in its own folder. We can look at past versions. Creating a new version just consists of copying the old one and then modifying it and incrementing the version number.
* openapi/ # OpenAPI stuff in here.
*
* schema/ # OpenAPI yaml files for each table in here.
* generated-ts-types/ # In here we place the OpenAPI-generated TS types.
* generated-zod/ # In here we place the openapi-zod-client generated Zod TS schema files.
* generated-cpp-source/ # In here we place the openapi-generator-cli generated C++ headers and JSON serdes.
* generated-odb-source/ # In here we place the ODB generated C++ ORM stuff.
* migrations/ # Subfolders here contain SQL statements to go from v to v.
* v-v/
* sqlite/ # Migrations SQL specific to SQLite.
* postgre/ # Migrations SQL specific to postgre.
The content in the generated-* subdirs will be added to git version control. The goal is to have them generated once on each schema change, and then accessible thereafter forever. So generating the content in the generated-* subdirs is not a standard build step. It's a separate build/make target which the dev must run manually when xe updates the schema. Perhaps we could have a build system step that checks the
git difforgit status, and if any files under the db/ subdir have been changed, we have CMake print a warning, advising the dev to create a new version and re-run the generation targets.There are 2 targets we want for the build system:
- Generate all generated-* content in the generated-* subdirs of the selected schema dir. The schema dir basename is supplied via CMake
-DDB_SCHEMA_DIR_TO_GENERATE="<schema_dir_name>". The name of the target that runs this should bedb_gen_orm_serdes_and_zod. - This one should be basically agnostic of what the "current version" is, because it should be supplied with 2 input arguments via
-DDB_SCHEMA_DIR_MIGRATION_FROM="<from_schema_dir_name>"and-DDB_SCHEMA_DIR_MIGRATION_TO="<to_schema_dir_name>". It should then output the migrations required to go from<from>to<to>inside of/db/migrations/<from>-<to>/. This build target's name should bedb_gen_migrations.
The name of the target that checks for git-tracked changes in the db/ subdir should be "db_check_schema_changes".
All of these build system targets should be built modularly, across multiple files to be placed in the top level /cmake subdir. Use subfunctions liberally. Split logically separate or repeated flows into their own callable functions. No long-ass CMake scrawl. For example, maybe each of the different generated-*/ subfolders has its own .cmake file with its specific functions, and there may be a dbGenerationCommon.cmake file with functions included and used by all of them in common.
Each of the generated-*/ subfolders should have its own CMake target, and all of them should be sub-targets of the main
db_gen_orm_serdes_and_zodtarget.The name of the top level "db" folder must be configurable as a CMake var -DCPPBESSOT_WORKDIR="foo", but its default value should be "db". The dev should be able to just change the value of that var and then manually rename the top-level dir, and the CMake scripts should run perfectly fine. There should be a separate .cmake file (in /cmake) which checks for the availability of the dependencies (odb, npx, openapi-zod-client, and the various openapi cli tools) and fails if they're not available.
The goal is for this C++ backend SSOT (project name: cppbessot) system to be a git submodule of multiple such C++ backend-based systems I'm making. So in practice, the cmake files in /cmake will be part of a cppbessot/ submodule of a larger project. The CPPBESSOT_WORKDIR will be a subdir of the larger project, while the CMake files will be in the submodule. In practice, the deployed system will look something like:
/bigproject CMakeLists.txt # Bigproject's own CMakeLists, which includes cppbessot/cmake/CppBeSSOT.cmake
- cmake/ # bigproject's own .cmake files.
- cppbessot/ # Git submodule containing the CppBeSSOT subproject and its .cmake files.
- ${CPPBESSOT_WORKDIR}
- v/
- migrations/
So we need a single CMake file that we can include, maybe called CppBeSSOT.cmake, while will include the other .cmake files, and make the cppbessot system available to the larger project.
- Generate all generated-* content in the generated-* subdirs of the selected schema dir. The schema dir basename is supplied via CMake