From 2787c7343cdce2addc1059d82bbe9034ff5acdeb Mon Sep 17 00:00:00 2001 From: Latent Prion Date: Sat, 28 Feb 2026 02:47:18 -0400 Subject: [PATCH] Add CppBeSsot implementation --- cmake/cppbessot/CppBeSSOT.cmake | 194 ++++++++++++++++++ cmake/cppbessot/README.md | 21 ++ cmake/cppbessot/dbDependencyCheck.cmake | 119 +++++++++++ cmake/cppbessot/dbGenCpp.cmake | 35 ++++ cmake/cppbessot/dbGenMigrations.cmake | 38 ++++ cmake/cppbessot/dbGenODB.cmake | 27 +++ cmake/cppbessot/dbGenSqlDDL.cmake | 27 +++ cmake/cppbessot/dbGenTS.cmake | 30 +++ cmake/cppbessot/dbGenZod.cmake | 31 +++ cmake/cppbessot/dbGenerationCommon.cmake | 114 ++++++++++ cmake/cppbessot/dbSchemaCheck.cmake | 26 +++ .../scripts/check_schema_changes.cmake | 50 +++++ cmake/cppbessot/scripts/run_odb_logic.cmake | 38 ++++ .../scripts/run_odb_migrations.cmake | 58 ++++++ cmake/cppbessot/scripts/run_odb_sql_ddl.cmake | 41 ++++ 15 files changed, 849 insertions(+) create mode 100644 cmake/cppbessot/CppBeSSOT.cmake create mode 100644 cmake/cppbessot/README.md create mode 100644 cmake/cppbessot/dbDependencyCheck.cmake create mode 100644 cmake/cppbessot/dbGenCpp.cmake create mode 100644 cmake/cppbessot/dbGenMigrations.cmake create mode 100644 cmake/cppbessot/dbGenODB.cmake create mode 100644 cmake/cppbessot/dbGenSqlDDL.cmake create mode 100644 cmake/cppbessot/dbGenTS.cmake create mode 100644 cmake/cppbessot/dbGenZod.cmake create mode 100644 cmake/cppbessot/dbGenerationCommon.cmake create mode 100644 cmake/cppbessot/dbSchemaCheck.cmake create mode 100644 cmake/cppbessot/scripts/check_schema_changes.cmake create mode 100644 cmake/cppbessot/scripts/run_odb_logic.cmake create mode 100644 cmake/cppbessot/scripts/run_odb_migrations.cmake create mode 100644 cmake/cppbessot/scripts/run_odb_sql_ddl.cmake diff --git a/cmake/cppbessot/CppBeSSOT.cmake b/cmake/cppbessot/CppBeSSOT.cmake new file mode 100644 index 0000000..7d23e48 --- /dev/null +++ b/cmake/cppbessot/CppBeSSOT.cmake @@ -0,0 +1,194 @@ +include_guard(GLOBAL) + +include(CMakeParseArguments) +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbDependencyCheck.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbSchemaCheck.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenTS.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenZod.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenCpp.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenODB.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenSqlDDL.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/dbGenMigrations.cmake") + +if(NOT DEFINED CPPBESSOT_WORKDIR) + set(CPPBESSOT_WORKDIR "db" CACHE STRING "CppBeSSOT schema root folder") +endif() + +if(NOT DEFINED DB_SCHEMA_VERSION_TO_GENERATE) + set(DB_SCHEMA_VERSION_TO_GENERATE "v1.1" CACHE STRING "Schema version to generate artifacts for") +endif() + +if(NOT DEFINED DB_SCHEMA_MIGRATION_VERSION_FROM) + set(DB_SCHEMA_MIGRATION_VERSION_FROM "" CACHE STRING + "Optional source schema version for migration generation (e.g. v1.1)") +endif() + +if(NOT DEFINED DB_SCHEMA_MIGRATION_VERSION_TO) + set(DB_SCHEMA_MIGRATION_VERSION_TO "" CACHE STRING + "Optional target schema version for migration generation (e.g. v1.2)") +endif() + +if(NOT DEFINED DB_SCHEMA_CHANGES_ARE_ERROR) + option(DB_SCHEMA_CHANGES_ARE_ERROR "Treat dirty schema changes as hard CMake error" OFF) +endif() + +if(NOT DEFINED CPPBESSOT_AUTO_ENABLE) + option(CPPBESSOT_AUTO_ENABLE "Auto-register CppBeSSOT targets when this file is included" ON) +endif() + +function(_cppbessot_try_link_nlohmann target_name) + # Purpose: Link a target to nlohmann_json when the imported target exists. + # Inputs: + # - target_name: CMake target name to link. + # Outputs: + # - Modifies target link interface; no-op when package target is absent. + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(${target_name} PUBLIC nlohmann_json::nlohmann_json) + endif() +endfunction() + +function(cppbessot_add_generated_libraries) + # Purpose: Create consumable libraries from generated model and ODB sources. + # Inputs: + # - VERSION (optional named arg): Schema version to consume. + # - DB_SCHEMA_VERSION_TO_GENERATE (fallback): Default schema version. + # Outputs: + # - Library targets (when sources exist): + # - cppBeSsotOpenAiModelGen + # - cppBeSsotOdbSqlite + # - cppBeSsotOdbPgSql + # - Alias targets: + # - cppbessot::openai_model_gen + # - cppbessot::odb_sqlite + # - cppbessot::odb_pgsql + # - Emits warnings if expected source sets are missing. + set(options) + set(one_value_args VERSION) + set(multi_value_args) + cmake_parse_arguments(CPPB "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN}) + + if(NOT CPPB_VERSION) + set(CPPB_VERSION "${DB_SCHEMA_VERSION_TO_GENERATE}") + endif() + + cppbessot_validate_schema_version("${CPPB_VERSION}") + cppbessot_get_version_dir(_version_dir "${CPPB_VERSION}") + + set(_cpp_include_dir "${_version_dir}/generated-cpp-source/include") + file(GLOB _model_include_dirs LIST_DIRECTORIES true "${_cpp_include_dir}/*/model") + + file(GLOB _model_sources CONFIGURE_DEPENDS + "${_version_dir}/generated-cpp-source/src/model/*.cpp") + if(_model_sources) + add_library(cppBeSsotOpenAiModelGen STATIC ${_model_sources}) + set_target_properties(cppBeSsotOpenAiModelGen PROPERTIES + OUTPUT_NAME "cppBeSsotOpenAiModelGen" + POSITION_INDEPENDENT_CODE ON) + target_include_directories(cppBeSsotOpenAiModelGen PUBLIC "${_cpp_include_dir}") + _cppbessot_try_link_nlohmann(cppBeSsotOpenAiModelGen) + add_library(cppbessot::openai_model_gen ALIAS cppBeSsotOpenAiModelGen) + else() + message(WARNING "No generated C++ model sources found for ${CPPB_VERSION}; skipping libcppBeSsotOpenAiModelGen.") + endif() + + file(GLOB _sqlite_odb_sources CONFIGURE_DEPENDS + "${_version_dir}/generated-odb-source/sqlite/*-odb.cxx") + if(_sqlite_odb_sources) + add_library(cppBeSsotOdbSqlite SHARED ${_sqlite_odb_sources}) + set_target_properties(cppBeSsotOdbSqlite PROPERTIES + OUTPUT_NAME "cppBeSsotOdbSqlite" + POSITION_INDEPENDENT_CODE ON) + target_include_directories(cppBeSsotOdbSqlite PUBLIC + "${_cpp_include_dir}" + "${_version_dir}/generated-odb-source/sqlite" + ${_model_include_dirs}) + add_library(cppbessot::odb_sqlite ALIAS cppBeSsotOdbSqlite) + else() + message(WARNING "No generated sqlite ODB sources found for ${CPPB_VERSION}; skipping libcppBeSsotOdbSqlite.") + endif() + + file(GLOB _pgsql_odb_sources CONFIGURE_DEPENDS + "${_version_dir}/generated-odb-source/postgre/*-odb.cxx") + if(_pgsql_odb_sources) + add_library(cppBeSsotOdbPgSql SHARED ${_pgsql_odb_sources}) + set_target_properties(cppBeSsotOdbPgSql PROPERTIES + OUTPUT_NAME "cppBeSsotOdbPgSql" + POSITION_INDEPENDENT_CODE ON) + target_include_directories(cppBeSsotOdbPgSql PUBLIC + "${_cpp_include_dir}" + "${_version_dir}/generated-odb-source/postgre" + ${_model_include_dirs}) + add_library(cppbessot::odb_pgsql ALIAS cppBeSsotOdbPgSql) + else() + message(WARNING "No generated postgre ODB sources found for ${CPPB_VERSION}; skipping libcppBeSsotOdbPgSql.") + endif() +endfunction() + +function(cppbessot_enable) + # Purpose: Entry-point orchestration for dependency checks, custom generation + # targets, aggregate targets, and generated library registration. + # Inputs: + # - CPPBESSOT_WORKDIR + # - DB_SCHEMA_VERSION_TO_GENERATE + # - DB_SCHEMA_MIGRATION_VERSION_FROM + # - DB_SCHEMA_MIGRATION_VERSION_TO + # - DB_SCHEMA_CHANGES_ARE_ERROR (optional behavior control) + # Outputs: + # - Custom targets: + # - db_check_schema_changes + # - db_gen_ts + # - db_gen_zod + # - db_gen_cpp_headers + # - db_gen_odb_logic + # - db_gen_sql_ddl + # - db_gen_migrations + # - db_gen_orm_serdes_and_zod + # - Generated library targets for selected schema version. + cppbessot_initialize_paths() + + cppbessot_validate_schema_version("${DB_SCHEMA_VERSION_TO_GENERATE}") + cppbessot_assert_version_dir_exists("${DB_SCHEMA_VERSION_TO_GENERATE}") + + cppbessot_check_dependencies() + + cppbessot_add_db_check_schema_changes_target() + cppbessot_add_db_gen_ts_target("${DB_SCHEMA_VERSION_TO_GENERATE}") + cppbessot_add_db_gen_zod_target("${DB_SCHEMA_VERSION_TO_GENERATE}") + cppbessot_add_db_gen_cpp_target("${DB_SCHEMA_VERSION_TO_GENERATE}") + cppbessot_add_db_gen_odb_target("${DB_SCHEMA_VERSION_TO_GENERATE}") + cppbessot_add_db_gen_sql_ddl_target("${DB_SCHEMA_VERSION_TO_GENERATE}") + if(NOT "${DB_SCHEMA_MIGRATION_VERSION_FROM}" STREQUAL "" + AND NOT "${DB_SCHEMA_MIGRATION_VERSION_TO}" STREQUAL "") + cppbessot_validate_schema_version("${DB_SCHEMA_MIGRATION_VERSION_FROM}") + cppbessot_validate_schema_version("${DB_SCHEMA_MIGRATION_VERSION_TO}") + cppbessot_assert_version_dir_exists("${DB_SCHEMA_MIGRATION_VERSION_FROM}") + cppbessot_assert_version_dir_exists("${DB_SCHEMA_MIGRATION_VERSION_TO}") + cppbessot_add_db_gen_migrations_target( + "${DB_SCHEMA_MIGRATION_VERSION_FROM}" + "${DB_SCHEMA_MIGRATION_VERSION_TO}") + else() + add_custom_target(db_gen_migrations + COMMAND "${CMAKE_COMMAND}" -E echo + "Set DB_SCHEMA_MIGRATION_VERSION_FROM and DB_SCHEMA_MIGRATION_VERSION_TO to enable migration generation." + COMMAND "${CMAKE_COMMAND}" -E false + VERBATIM + ) + set_target_properties(db_gen_migrations PROPERTIES EXCLUDE_FROM_ALL TRUE) + endif() + + add_custom_target(db_gen_orm_serdes_and_zod) + add_dependencies(db_gen_orm_serdes_and_zod + db_gen_ts + db_gen_zod + db_gen_cpp_headers + db_gen_odb_logic + db_gen_sql_ddl) + set_target_properties(db_gen_orm_serdes_and_zod PROPERTIES EXCLUDE_FROM_ALL TRUE) + + cppbessot_add_generated_libraries(VERSION "${DB_SCHEMA_VERSION_TO_GENERATE}") +endfunction() + +if(CPPBESSOT_AUTO_ENABLE) + cppbessot_enable() +endif() diff --git a/cmake/cppbessot/README.md b/cmake/cppbessot/README.md new file mode 100644 index 0000000..18e2357 --- /dev/null +++ b/cmake/cppbessot/README.md @@ -0,0 +1,21 @@ +# CppBeSSOT CMake Module + +## Quick include from a parent project + +```cmake +# Optional overrides before include: +set(CPPBESSOT_WORKDIR "db") +set(DB_SCHEMA_VERSION_TO_GENERATE "v1.1") +set(DB_SCHEMA_MIGRATION_VERSION_FROM "v1.1") +set(DB_SCHEMA_MIGRATION_VERSION_TO "v1.2") + +include(path/to/cppbessot/cmake/cppbessot/CppBeSSOT.cmake) +``` + +By default the include auto-registers targets and libraries. To disable auto setup: + +```cmake +set(CPPBESSOT_AUTO_ENABLE OFF) +include(path/to/cppbessot/cmake/cppbessot/CppBeSSOT.cmake) +cppbessot_enable() +``` diff --git a/cmake/cppbessot/dbDependencyCheck.cmake b/cmake/cppbessot/dbDependencyCheck.cmake new file mode 100644 index 0000000..5dd93d5 --- /dev/null +++ b/cmake/cppbessot/dbDependencyCheck.cmake @@ -0,0 +1,119 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") +include(CheckIncludeFileCXX) + +function(_cppbessot_require_program var_name program_name hint) + # Purpose: Locate an executable and fail with a clear install hint if missing. + # Inputs: + # - var_name: Variable name to store executable path. + # - program_name: Program to search in PATH. + # - hint: Human-readable installation guidance. + # Outputs: + # - : Absolute executable path (in current scope). + # - No return value; raises FATAL_ERROR if program is not found. + find_program(${var_name} ${program_name}) + if(NOT ${var_name}) + message(FATAL_ERROR + "Missing required tool `${program_name}`. ${hint}") + endif() +endfunction() + +function(_cppbessot_require_npm_package npm_executable package_name) + # Purpose: Ensure an npm package exists either locally in PROJECT_SOURCE_DIR + # or globally in the active npm installation. + # Inputs: + # - npm_executable: Path to npm. + # - package_name: Package name to validate. + # Outputs: + # - No return value; raises FATAL_ERROR when package is not installed. + execute_process( + COMMAND "${npm_executable}" list --depth=0 "${package_name}" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE _local_result + OUTPUT_QUIET + ERROR_QUIET + ) + + if(NOT _local_result EQUAL 0) + execute_process( + COMMAND "${npm_executable}" list -g --depth=0 "${package_name}" + RESULT_VARIABLE _global_result + OUTPUT_QUIET + ERROR_QUIET + ) + else() + set(_global_result 0) + endif() + + if(NOT _local_result EQUAL 0 AND NOT _global_result EQUAL 0) + message(FATAL_ERROR + "${package_name} is not installed (not found in local or global npm packages). " + "Install with `npm i -D ${package_name}` (project-local) or `npm i -g ${package_name}` (global).") + endif() +endfunction() + +function(_cppbessot_require_npx_package_executable npx_executable package_executable) + # Purpose: Ensure npx can execute a package-provided executable without network + # resolution/download (`--no-install`). + # Inputs: + # - npx_executable: Path to npx. + # - package_executable: Executable name exposed by a package. + # Outputs: + # - No return value; raises FATAL_ERROR if execution fails. + execute_process( + COMMAND "${npx_executable}" --no-install "${package_executable}" --help + RESULT_VARIABLE _exec_result + OUTPUT_QUIET + ERROR_VARIABLE _exec_stderr + ) + if(NOT _exec_result EQUAL 0) + message(FATAL_ERROR + "${package_executable} is not available through npx. " + "Ensure the supplying package is installed locally or globally. " + "Underlying error: ${_exec_stderr}") + endif() +endfunction() + +function(cppbessot_check_dependencies) + # Purpose: Validate required external tools and nlohmann/json availability. + # Inputs: + # - None (uses PATH/toolchain + optional find_package results). + # Outputs: + # - CPPBESSOT_ODB_EXECUTABLE (PARENT_SCOPE) + # - CPPBESSOT_NPX_EXECUTABLE (PARENT_SCOPE) + # - CPPBESSOT_NPM_EXECUTABLE (PARENT_SCOPE) + # - CPPBESSOT_JAVA_EXECUTABLE (PARENT_SCOPE) + # - CPPBESSOT_GIT_EXECUTABLE (PARENT_SCOPE) + # - CPPBESSOT_OPENAPI_ZOD_AVAILABLE (PARENT_SCOPE) + # - No return value; raises FATAL_ERROR on missing dependencies. + _cppbessot_require_program(CPPBESSOT_ODB_EXECUTABLE odb + "Install ODB compiler and ensure `odb` is in PATH.") + _cppbessot_require_program(CPPBESSOT_NPX_EXECUTABLE npx + "Install Node.js/NPM so `npx` is available.") + _cppbessot_require_program(CPPBESSOT_NPM_EXECUTABLE npm + "Install Node.js/NPM so `npm` is available.") + _cppbessot_require_program(CPPBESSOT_JAVA_EXECUTABLE java + "Install a Java runtime (OpenAPI generator uses Java).") + _cppbessot_require_program(CPPBESSOT_GIT_EXECUTABLE git + "Install Git and ensure it is available in PATH.") + + _cppbessot_require_npm_package("${CPPBESSOT_NPM_EXECUTABLE}" "openapi-zod-client") + _cppbessot_require_npx_package_executable("${CPPBESSOT_NPX_EXECUTABLE}" "openapi-zod-client") + + find_package(nlohmann_json QUIET) + if(NOT nlohmann_json_FOUND) + check_include_file_cxx("nlohmann/json.hpp" CPPBESSOT_HAS_NLOHMANN_JSON_HEADER) + if(NOT CPPBESSOT_HAS_NLOHMANN_JSON_HEADER) + message(FATAL_ERROR + "nlohmann/json headers were not found. On Ubuntu/Debian: `sudo apt install nlohmann-json3-dev`.") + endif() + endif() + + set(CPPBESSOT_ODB_EXECUTABLE "${CPPBESSOT_ODB_EXECUTABLE}" PARENT_SCOPE) + set(CPPBESSOT_NPX_EXECUTABLE "${CPPBESSOT_NPX_EXECUTABLE}" PARENT_SCOPE) + set(CPPBESSOT_NPM_EXECUTABLE "${CPPBESSOT_NPM_EXECUTABLE}" PARENT_SCOPE) + set(CPPBESSOT_JAVA_EXECUTABLE "${CPPBESSOT_JAVA_EXECUTABLE}" PARENT_SCOPE) + set(CPPBESSOT_GIT_EXECUTABLE "${CPPBESSOT_GIT_EXECUTABLE}" PARENT_SCOPE) + set(CPPBESSOT_OPENAPI_ZOD_AVAILABLE TRUE PARENT_SCOPE) +endfunction() diff --git a/cmake/cppbessot/dbGenCpp.cmake b/cmake/cppbessot/dbGenCpp.cmake new file mode 100644 index 0000000..fa012c9 --- /dev/null +++ b/cmake/cppbessot/dbGenCpp.cmake @@ -0,0 +1,35 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_cpp_target version) + # Purpose: Register C++ model generation target using checked-in templates. + # Inputs: + # - version: Schema version to generate for. + # - CPPBESSOT_NPX_EXECUTABLE: Path to `npx`. + # Outputs: + # - CMake target: `db_gen_cpp_headers` (EXCLUDE_FROM_ALL). + # - Files under `/generated-cpp-source`. + cppbessot_validate_schema_version("${version}") + cppbessot_get_version_dir(_version_dir "${version}") + + set(_openapi_file "${_version_dir}/openapi/openapi.yaml") + set(_template_dir "${_version_dir}/openapi/templates/cpp-odb-json") + set(_template_config "${_template_dir}/config.yaml") + set(_output_dir "${_version_dir}/generated-cpp-source") + + add_custom_target(db_gen_cpp_headers + COMMAND ${CMAKE_COMMAND} -E make_directory "${_output_dir}" + COMMAND "${CPPBESSOT_NPX_EXECUTABLE}" @openapitools/openapi-generator-cli generate + -i "${_openapi_file}" + -g cpp-restsdk + -t "${_template_dir}" + -c "${_template_config}" + -o "${_output_dir}" + --global-property models + COMMENT "Generating C++ model headers/sources for ${version}" + VERBATIM + ) + + set_target_properties(db_gen_cpp_headers PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenMigrations.cmake b/cmake/cppbessot/dbGenMigrations.cmake new file mode 100644 index 0000000..5043b7f --- /dev/null +++ b/cmake/cppbessot/dbGenMigrations.cmake @@ -0,0 +1,38 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_migrations_target from_version to_version) + # Purpose: Register migration SQL generation between two schema versions. + # Inputs: + # - from_version: Source schema version (changelog input side). + # - to_version: Target schema version (header/changelog output side). + # - CPPBESSOT_ODB_EXECUTABLE: Path to `odb` compiler. + # Outputs: + # - CMake target: `db_gen_migrations` (EXCLUDE_FROM_ALL). + # - Files under `migrations/-/{sqlite,postgre}`. + cppbessot_validate_schema_version("${from_version}") + cppbessot_validate_schema_version("${to_version}") + + if("${from_version}" STREQUAL "${to_version}") + message(FATAL_ERROR "Migration `from` and `to` versions must differ.") + endif() + + cppbessot_get_version_dir(_from_dir "${from_version}") + cppbessot_get_version_dir(_to_dir "${to_version}") + cppbessot_abs_path(_workdir "${CPPBESSOT_WORKDIR}") + set(_migration_dir "${_workdir}/migrations/${from_version}-${to_version}") + + add_custom_target(db_gen_migrations + COMMAND "${CMAKE_COMMAND}" + -DCPPBESSOT_ODB_EXECUTABLE="${CPPBESSOT_ODB_EXECUTABLE}" + -DCPPBESSOT_FROM_VERSION_DIR="${_from_dir}" + -DCPPBESSOT_TO_VERSION_DIR="${_to_dir}" + -DCPPBESSOT_MIGRATION_DIR="${_migration_dir}" + -P "${CMAKE_CURRENT_LIST_DIR}/scripts/run_odb_migrations.cmake" + COMMENT "Generating DB migrations: ${from_version} -> ${to_version}" + VERBATIM + ) + + set_target_properties(db_gen_migrations PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenODB.cmake b/cmake/cppbessot/dbGenODB.cmake new file mode 100644 index 0000000..572a45a --- /dev/null +++ b/cmake/cppbessot/dbGenODB.cmake @@ -0,0 +1,27 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_odb_target version) + # Purpose: Register ODB ORM generation target for sqlite and postgre backends. + # Inputs: + # - version: Schema version to generate for. + # - CPPBESSOT_ODB_EXECUTABLE: Path to `odb` compiler. + # Outputs: + # - CMake target: `db_gen_odb_logic` (EXCLUDE_FROM_ALL). + # - Files under `/generated-odb-source/{sqlite,postgre}`. + cppbessot_validate_schema_version("${version}") + cppbessot_get_version_dir(_version_dir "${version}") + + add_custom_target(db_gen_odb_logic + COMMAND "${CMAKE_COMMAND}" + -DCPPBESSOT_ODB_EXECUTABLE="${CPPBESSOT_ODB_EXECUTABLE}" + -DCPPBESSOT_VERSION_DIR="${_version_dir}" + -P "${CMAKE_CURRENT_LIST_DIR}/scripts/run_odb_logic.cmake" + DEPENDS db_gen_cpp_headers + COMMENT "Generating ODB ORM sources for ${version} (sqlite + postgre)" + VERBATIM + ) + + set_target_properties(db_gen_odb_logic PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenSqlDDL.cmake b/cmake/cppbessot/dbGenSqlDDL.cmake new file mode 100644 index 0000000..72ab554 --- /dev/null +++ b/cmake/cppbessot/dbGenSqlDDL.cmake @@ -0,0 +1,27 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_sql_ddl_target version) + # Purpose: Register SQL DDL snapshot generation target for supported backends. + # Inputs: + # - version: Schema version to generate for. + # - CPPBESSOT_ODB_EXECUTABLE: Path to `odb` compiler. + # Outputs: + # - CMake target: `db_gen_sql_ddl` (EXCLUDE_FROM_ALL). + # - Files under `/generated-sql-ddl/{sqlite,postgre}`. + cppbessot_validate_schema_version("${version}") + cppbessot_get_version_dir(_version_dir "${version}") + + add_custom_target(db_gen_sql_ddl + COMMAND "${CMAKE_COMMAND}" + -DCPPBESSOT_ODB_EXECUTABLE="${CPPBESSOT_ODB_EXECUTABLE}" + -DCPPBESSOT_VERSION_DIR="${_version_dir}" + -P "${CMAKE_CURRENT_LIST_DIR}/scripts/run_odb_sql_ddl.cmake" + DEPENDS db_gen_cpp_headers + COMMENT "Generating SQL DDL snapshots for ${version} (sqlite + postgre)" + VERBATIM + ) + + set_target_properties(db_gen_sql_ddl PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenTS.cmake b/cmake/cppbessot/dbGenTS.cmake new file mode 100644 index 0000000..2768cac --- /dev/null +++ b/cmake/cppbessot/dbGenTS.cmake @@ -0,0 +1,30 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_ts_target version) + # Purpose: Register TypeScript type generation target from OpenAPI input. + # Inputs: + # - version: Schema version to generate for. + # - CPPBESSOT_NPX_EXECUTABLE: Path to `npx`. + # Outputs: + # - CMake target: `db_gen_ts` (EXCLUDE_FROM_ALL). + # - Files under `/generated-ts-types`. + cppbessot_validate_schema_version("${version}") + cppbessot_get_version_dir(_version_dir "${version}") + + set(_openapi_file "${_version_dir}/openapi/openapi.yaml") + set(_output_dir "${_version_dir}/generated-ts-types") + + add_custom_target(db_gen_ts + COMMAND ${CMAKE_COMMAND} -E make_directory "${_output_dir}" + COMMAND "${CPPBESSOT_NPX_EXECUTABLE}" @openapitools/openapi-generator-cli generate + -i "${_openapi_file}" + -g typescript-fetch + -o "${_output_dir}" + COMMENT "Generating TypeScript types for ${version}" + VERBATIM + ) + + set_target_properties(db_gen_ts PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenZod.cmake b/cmake/cppbessot/dbGenZod.cmake new file mode 100644 index 0000000..85bebc3 --- /dev/null +++ b/cmake/cppbessot/dbGenZod.cmake @@ -0,0 +1,31 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_gen_zod_target version) + # Purpose: Register Zod schema generation target from OpenAPI input. + # Inputs: + # - version: Schema version to generate for. + # - CPPBESSOT_NPX_EXECUTABLE: Path to `npx`. + # Outputs: + # - CMake target: `db_gen_zod` (EXCLUDE_FROM_ALL). + # - File `/generated-zod/schemas.ts`. + cppbessot_validate_schema_version("${version}") + cppbessot_get_version_dir(_version_dir "${version}") + + set(_openapi_file "${_version_dir}/openapi/openapi.yaml") + set(_output_dir "${_version_dir}/generated-zod") + set(_output_file "${_output_dir}/schemas.ts") + + add_custom_target(db_gen_zod + COMMAND ${CMAKE_COMMAND} -E make_directory "${_output_dir}" + COMMAND "${CPPBESSOT_NPX_EXECUTABLE}" --no-install openapi-zod-client + --input "${_openapi_file}" + --output "${_output_file}" + --export-schemas + COMMENT "Generating Zod schemas for ${version}" + VERBATIM + ) + + set_target_properties(db_gen_zod PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/dbGenerationCommon.cmake b/cmake/cppbessot/dbGenerationCommon.cmake new file mode 100644 index 0000000..fd954bd --- /dev/null +++ b/cmake/cppbessot/dbGenerationCommon.cmake @@ -0,0 +1,114 @@ +include_guard(GLOBAL) + +if(NOT DEFINED CPPBESSOT_WORKDIR) + set(CPPBESSOT_WORKDIR "db" CACHE STRING "CppBeSSOT schema root folder, relative to PROJECT_SOURCE_DIR or absolute path") +endif() + +function(cppbessot_abs_path out_var input_path) + # Purpose: Resolve a path to an absolute path anchored at PROJECT_SOURCE_DIR + # when the input is relative. + # Inputs: + # - out_var: Name of the parent-scope variable to write. + # - input_path: Relative or absolute input path. + # Outputs: + # - (PARENT_SCOPE): Absolute resolved path. + if(IS_ABSOLUTE "${input_path}") + set(_resolved "${input_path}") + else() + set(_resolved "${PROJECT_SOURCE_DIR}/${input_path}") + endif() + + get_filename_component(_resolved "${_resolved}" ABSOLUTE) + set(${out_var} "${_resolved}" PARENT_SCOPE) +endfunction() + +function(cppbessot_initialize_paths) + # Purpose: Initialize commonly used CppBeSSOT module and workdir paths. + # Inputs: + # - CPPBESSOT_WORKDIR (cache/normal variable): Configured schema root path. + # Outputs: + # - CPPBESSOT_CMAKE_DIR (PARENT_SCOPE): Absolute module directory path. + # - CPPBESSOT_MODULE_ROOT (PARENT_SCOPE): Module root directory path. + # - CPPBESSOT_WORKDIR_ABS (PARENT_SCOPE): Absolute schema root path. + get_filename_component(CPPBESSOT_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}" ABSOLUTE) + get_filename_component(CPPBESSOT_MODULE_ROOT "${CPPBESSOT_CMAKE_DIR}/../.." ABSOLUTE) + cppbessot_abs_path(CPPBESSOT_WORKDIR_ABS "${CPPBESSOT_WORKDIR}") + + set(CPPBESSOT_CMAKE_DIR "${CPPBESSOT_CMAKE_DIR}" PARENT_SCOPE) + set(CPPBESSOT_MODULE_ROOT "${CPPBESSOT_MODULE_ROOT}" PARENT_SCOPE) + set(CPPBESSOT_WORKDIR_ABS "${CPPBESSOT_WORKDIR_ABS}" PARENT_SCOPE) +endfunction() + +function(cppbessot_require_var var_name) + # Purpose: Fail fast if a required CMake variable is missing/empty. + # Inputs: + # - var_name: Variable name to validate. + # Outputs: + # - No return value; raises FATAL_ERROR on invalid input. + if(NOT DEFINED ${var_name} OR "${${var_name}}" STREQUAL "") + message(FATAL_ERROR "Missing required CMake variable `${var_name}`.") + endif() +endfunction() + +function(cppbessot_validate_schema_version version) + # Purpose: Validate schema version format and enforce major >= 1. + # Inputs: + # - version: Expected format `v.` (e.g. v1.1). + # Outputs: + # - No return value; raises FATAL_ERROR if format is invalid. + if(NOT "${version}" MATCHES "^v([1-9][0-9]*)\\.([0-9]+)$") + message(FATAL_ERROR + "Invalid schema version `${version}`. Expected format `v.` with major >= 1 (e.g. v1.1, v1.2).") + endif() +endfunction() + +function(cppbessot_get_version_parts version out_major out_minor) + # Purpose: Parse schema version string into numeric major/minor components. + # Inputs: + # - version: Schema version string. + # - out_major: Parent-scope variable name for major part. + # - out_minor: Parent-scope variable name for minor part. + # Outputs: + # - (PARENT_SCOPE): Major component. + # - (PARENT_SCOPE): Minor component. + cppbessot_validate_schema_version("${version}") + string(REGEX REPLACE "^v([1-9][0-9]*)\\.([0-9]+)$" "\\1" _major "${version}") + string(REGEX REPLACE "^v([1-9][0-9]*)\\.([0-9]+)$" "\\2" _minor "${version}") + set(${out_major} "${_major}" PARENT_SCOPE) + set(${out_minor} "${_minor}" PARENT_SCOPE) +endfunction() + +function(cppbessot_get_version_dir out_var version) + # Purpose: Resolve the absolute folder path for a specific schema version. + # Inputs: + # - out_var: Parent-scope variable name to receive the path. + # - version: Schema version string. + # Outputs: + # - (PARENT_SCOPE): Absolute `${CPPBESSOT_WORKDIR}/` path. + cppbessot_validate_schema_version("${version}") + cppbessot_abs_path(_workdir "${CPPBESSOT_WORKDIR}") + set(${out_var} "${_workdir}/${version}" PARENT_SCOPE) +endfunction() + +function(cppbessot_assert_version_dir_exists version) + # Purpose: Assert that a schema version directory exists on disk. + # Inputs: + # - version: Schema version string. + # Outputs: + # - No return value; raises FATAL_ERROR if directory is missing. + cppbessot_get_version_dir(_version_dir "${version}") + if(NOT IS_DIRECTORY "${_version_dir}") + message(FATAL_ERROR "Schema version folder does not exist: ${_version_dir}") + endif() +endfunction() + +function(cppbessot_get_model_headers_glob out_var version) + # Purpose: Build a model-header glob expression for a schema version. + # Inputs: + # - out_var: Parent-scope variable name to receive the glob pattern. + # - version: Schema version string. + # Outputs: + # - (PARENT_SCOPE): Glob pattern for generated model headers. + cppbessot_get_version_dir(_version_dir "${version}") + set(${out_var} "${_version_dir}/generated-cpp-source/include/*/model/*.h" PARENT_SCOPE) +endfunction() diff --git a/cmake/cppbessot/dbSchemaCheck.cmake b/cmake/cppbessot/dbSchemaCheck.cmake new file mode 100644 index 0000000..7906028 --- /dev/null +++ b/cmake/cppbessot/dbSchemaCheck.cmake @@ -0,0 +1,26 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/dbGenerationCommon.cmake") + +function(cppbessot_add_db_check_schema_changes_target) + # Purpose: Register a manual target that reports git-tracked schema changes. + # Inputs: + # - CPPBESSOT_WORKDIR: Schema root directory (relative or absolute). + # - CPPBESSOT_GIT_EXECUTABLE: Git executable path (set by dependency check). + # Outputs: + # - CMake target: `db_check_schema_changes` (EXCLUDE_FROM_ALL). + # - Runtime warning/send-error from the script when changes are detected. + cppbessot_abs_path(_workdir "${CPPBESSOT_WORKDIR}") + + add_custom_target(db_check_schema_changes + COMMAND "${CMAKE_COMMAND}" + -DCPPBESSOT_GIT_EXECUTABLE="${CPPBESSOT_GIT_EXECUTABLE}" + -DCPPBESSOT_WORKDIR_ABS="${_workdir}" + -DCPPBESSOT_PROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}" + -P "${CMAKE_CURRENT_LIST_DIR}/scripts/check_schema_changes.cmake" + COMMENT "Checking for schema changes under ${CPPBESSOT_WORKDIR}" + VERBATIM + ) + + set_target_properties(db_check_schema_changes PROPERTIES EXCLUDE_FROM_ALL TRUE) +endfunction() diff --git a/cmake/cppbessot/scripts/check_schema_changes.cmake b/cmake/cppbessot/scripts/check_schema_changes.cmake new file mode 100644 index 0000000..06c392d --- /dev/null +++ b/cmake/cppbessot/scripts/check_schema_changes.cmake @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT DEFINED CPPBESSOT_GIT_EXECUTABLE OR CPPBESSOT_GIT_EXECUTABLE STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_GIT_EXECUTABLE is required") +endif() +if(NOT DEFINED CPPBESSOT_WORKDIR_ABS OR CPPBESSOT_WORKDIR_ABS STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_WORKDIR_ABS is required") +endif() +if(NOT DEFINED CPPBESSOT_PROJECT_SOURCE_DIR OR CPPBESSOT_PROJECT_SOURCE_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_PROJECT_SOURCE_DIR is required") +endif() + +execute_process( + COMMAND "${CPPBESSOT_GIT_EXECUTABLE}" -C "${CPPBESSOT_PROJECT_SOURCE_DIR}" rev-parse --show-toplevel + RESULT_VARIABLE _top_result + OUTPUT_VARIABLE _git_toplevel + ERROR_VARIABLE _top_stderr +) + +if(NOT _top_result EQUAL 0) + message(FATAL_ERROR "git rev-parse failed while checking schema changes.\n${_top_stderr}") +endif() + +string(STRIP "${_git_toplevel}" _git_toplevel) +file(RELATIVE_PATH _workdir_rel "${_git_toplevel}" "${CPPBESSOT_WORKDIR_ABS}") +if(_workdir_rel MATCHES "^\\.\\.") + set(_workdir_rel "${CPPBESSOT_WORKDIR_ABS}") +endif() + +execute_process( + COMMAND "${CPPBESSOT_GIT_EXECUTABLE}" -C "${CPPBESSOT_PROJECT_SOURCE_DIR}" status --porcelain -- "${_workdir_rel}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _output + ERROR_VARIABLE _stderr +) + +if(NOT _result EQUAL 0) + message(FATAL_ERROR "git status failed while checking schema changes.\n${_stderr}") +endif() + +string(STRIP "${_output}" _output) +if(NOT _output STREQUAL "") + if(DEFINED DB_SCHEMA_CHANGES_ARE_ERROR AND DB_SCHEMA_CHANGES_ARE_ERROR) + message(SEND_ERROR + "Detected changes under `${CPPBESSOT_WORKDIR_ABS}`. Create a new schema version folder and regenerate artifacts.") + else() + message(WARNING + "Detected changes under `${CPPBESSOT_WORKDIR_ABS}`. Consider creating a new schema version folder and regenerating artifacts.") + endif() +endif() diff --git a/cmake/cppbessot/scripts/run_odb_logic.cmake b/cmake/cppbessot/scripts/run_odb_logic.cmake new file mode 100644 index 0000000..b117493 --- /dev/null +++ b/cmake/cppbessot/scripts/run_odb_logic.cmake @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT DEFINED CPPBESSOT_ODB_EXECUTABLE OR CPPBESSOT_ODB_EXECUTABLE STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_ODB_EXECUTABLE is required") +endif() +if(NOT DEFINED CPPBESSOT_VERSION_DIR OR CPPBESSOT_VERSION_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_VERSION_DIR is required") +endif() + +set(_include_dir "${CPPBESSOT_VERSION_DIR}/generated-cpp-source/include") +file(GLOB _headers "${_include_dir}/*/model/*.h") +if(NOT _headers) + message(FATAL_ERROR "No model headers found under ${_include_dir}") +endif() + +foreach(_backend IN ITEMS sqlite pgsql) + if(_backend STREQUAL "sqlite") + set(_subdir sqlite) + else() + set(_subdir postgre) + endif() + + set(_out_dir "${CPPBESSOT_VERSION_DIR}/generated-odb-source/${_subdir}") + file(MAKE_DIRECTORY "${_out_dir}") + + execute_process( + COMMAND "${CPPBESSOT_ODB_EXECUTABLE}" -I "${_include_dir}" --std c++11 -d "${_backend}" -q + -o "${_out_dir}" --changelog-dir "${_out_dir}" ${_headers} + RESULT_VARIABLE _result + OUTPUT_VARIABLE _stdout + ERROR_VARIABLE _stderr + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR + "ODB ORM generation failed for backend `${_backend}`.\n${_stdout}\n${_stderr}") + endif() +endforeach() diff --git a/cmake/cppbessot/scripts/run_odb_migrations.cmake b/cmake/cppbessot/scripts/run_odb_migrations.cmake new file mode 100644 index 0000000..e291cb6 --- /dev/null +++ b/cmake/cppbessot/scripts/run_odb_migrations.cmake @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT DEFINED CPPBESSOT_ODB_EXECUTABLE OR CPPBESSOT_ODB_EXECUTABLE STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_ODB_EXECUTABLE is required") +endif() +if(NOT DEFINED CPPBESSOT_FROM_VERSION_DIR OR CPPBESSOT_FROM_VERSION_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_FROM_VERSION_DIR is required") +endif() +if(NOT DEFINED CPPBESSOT_TO_VERSION_DIR OR CPPBESSOT_TO_VERSION_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_TO_VERSION_DIR is required") +endif() +if(NOT DEFINED CPPBESSOT_MIGRATION_DIR OR CPPBESSOT_MIGRATION_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_MIGRATION_DIR is required") +endif() + +set(_to_include_dir "${CPPBESSOT_TO_VERSION_DIR}/generated-cpp-source/include") +file(GLOB _to_headers "${_to_include_dir}/*/model/*.h") +if(NOT _to_headers) + message(FATAL_ERROR "No target-version headers found under ${_to_include_dir}") +endif() + +foreach(_backend IN ITEMS sqlite pgsql) + if(_backend STREQUAL "sqlite") + set(_subdir sqlite) + else() + set(_subdir postgre) + endif() + + set(_migration_backend_dir "${CPPBESSOT_MIGRATION_DIR}/${_subdir}") + file(MAKE_DIRECTORY "${_migration_backend_dir}") + + foreach(_header IN LISTS _to_headers) + get_filename_component(_name "${_header}" NAME_WE) + set(_in_xml "${CPPBESSOT_FROM_VERSION_DIR}/generated-odb-source/${_subdir}/${_name}.xml") + set(_out_xml "${CPPBESSOT_TO_VERSION_DIR}/generated-odb-source/${_subdir}/${_name}.xml") + + if(NOT EXISTS "${_in_xml}") + message(FATAL_ERROR "Missing changelog input for `${_name}`: ${_in_xml}") + endif() + + execute_process( + COMMAND "${CPPBESSOT_ODB_EXECUTABLE}" -I "${_to_include_dir}" --std c++11 -d "${_backend}" + --generate-schema --schema-format sql -q + -o "${_migration_backend_dir}" + --changelog-in "${_in_xml}" + --changelog-out "${_out_xml}" + "${_header}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _stdout + ERROR_VARIABLE _stderr + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR + "Migration generation failed for `${_name}` backend `${_backend}`.\n${_stdout}\n${_stderr}") + endif() + endforeach() +endforeach() diff --git a/cmake/cppbessot/scripts/run_odb_sql_ddl.cmake b/cmake/cppbessot/scripts/run_odb_sql_ddl.cmake new file mode 100644 index 0000000..b68ea94 --- /dev/null +++ b/cmake/cppbessot/scripts/run_odb_sql_ddl.cmake @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT DEFINED CPPBESSOT_ODB_EXECUTABLE OR CPPBESSOT_ODB_EXECUTABLE STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_ODB_EXECUTABLE is required") +endif() +if(NOT DEFINED CPPBESSOT_VERSION_DIR OR CPPBESSOT_VERSION_DIR STREQUAL "") + message(FATAL_ERROR "CPPBESSOT_VERSION_DIR is required") +endif() + +set(_include_dir "${CPPBESSOT_VERSION_DIR}/generated-cpp-source/include") +file(GLOB _headers "${_include_dir}/*/model/*.h") +if(NOT _headers) + message(FATAL_ERROR "No model headers found under ${_include_dir}") +endif() + +foreach(_backend IN ITEMS sqlite pgsql) + if(_backend STREQUAL "sqlite") + set(_subdir sqlite) + else() + set(_subdir postgre) + endif() + + set(_ddl_dir "${CPPBESSOT_VERSION_DIR}/generated-sql-ddl/${_subdir}") + set(_changelog_dir "${CPPBESSOT_VERSION_DIR}/generated-odb-source/${_subdir}") + file(MAKE_DIRECTORY "${_ddl_dir}") + file(MAKE_DIRECTORY "${_changelog_dir}") + + execute_process( + COMMAND "${CPPBESSOT_ODB_EXECUTABLE}" -I "${_include_dir}" --std c++11 -d "${_backend}" + --generate-schema --schema-format sql -q + -o "${_ddl_dir}" --changelog-dir "${_changelog_dir}" ${_headers} + RESULT_VARIABLE _result + OUTPUT_VARIABLE _stdout + ERROR_VARIABLE _stderr + ) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR + "ODB SQL DDL generation failed for backend `${_backend}`.\n${_stdout}\n${_stderr}") + endif() +endforeach()