diff --git a/CMakeLists.txt b/CMakeLists.txt index dea56f626..5fe2ff93e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,10 @@ set(minReqMsvcVersion 19.29) set(minRecGCCVersion 10.2) set(minRecClangVersion 12) # Packages -set(minQtVersion 5.15.2) -set(minRangeV3Version 0.11.0) +set(minQtVersion 5.15.2) +set(minRangeV3Version 0.11.0) +# tabulate doesn't provide Package Version File +#set(minTabulateVersion 1.4.0) # Make minimum toolchain version a requirement tiny_toolchain_requirement( @@ -157,11 +159,25 @@ option is disabled, then only the query builder without ORM is compiled." DISABLED TINYORM_DISABLE_ORM ) +target_optional_compile_definitions(${TinyOrm_target} + PUBLIC + FEATURE NAME TOM + DEFAULT ON + DESCRIPTION "Controls the compilation of all tom related source code (command \ +line interface)." + DISABLED TINYORM_DISABLE_TOM +) +# Depends on the TOM option so defined here +feature_option_dependent(TOM_EXAMPLE + "Build the tom command-line application example" OFF + "TOM" TOM_EXAMPLE-NOTFOUND +) + # TinyORM library header and source files # --- include(TinySources) -tiny_sources(${TinyOrm_target}_headers ${TinyOrm_target}_sources) +tinyorm_sources(${TinyOrm_target}_headers ${TinyOrm_target}_sources) target_sources(${TinyOrm_target} PRIVATE ${${TinyOrm_target}_headers} @@ -212,7 +228,6 @@ target_compile_definitions(${TinyOrm_target} $<$>:TINYORM_NO_DEBUG> # Do not log queries $<$>:TINYORM_NO_DEBUG_SQL> - PRIVATE # Debug build $<$:TINYORM_DEBUG> # Log queries with a time measurement @@ -237,10 +252,49 @@ else() endif() # Enable code needed by tests, eg. connection overriding in the Model -if(BUILD_TESTS) +if(BUILD_TESTS AND ORM) target_compile_definitions(${TinyOrm_target} PUBLIC TINYORM_TESTS_CODE) endif() +# TinyTom related header and source files +# --- + +if(TOM) + tinytom_sources(${TomExample_target}_headers ${TomExample_target}_sources) + + target_sources(${TinyOrm_target} PRIVATE + ${${TomExample_target}_headers} + ${${TomExample_target}_sources} + ) +endif() + +# TinyTom related specific configuration +# --- + +if(TOM) + target_include_directories(${TinyOrm_target} PUBLIC + "$" + ) +endif() + +# TinyTom related defines +# --- + +if(TOM) + target_compile_definitions(${TinyOrm_target} + PUBLIC + # Release build + $<$>:TINYTOM_NO_DEBUG> + # Debug build + $<$:TINYTOM_DEBUG> + ) + + # Enable code needed by tests (modify the migrate:status command for tests need) + if(BUILD_TESTS) + target_compile_definitions(${TinyOrm_target} PUBLIC TINYTOM_TESTS_CODE) + endif() +endif() + # Windows resource and manifest files # --- @@ -285,6 +339,12 @@ if(MYSQL_PING) target_link_libraries(${TinyOrm_target} PRIVATE MySQL::MySQL) endif() +if(TOM) + # tabulate doesn't provide Package Version File + tiny_find_package(tabulate CONFIG REQUIRED) + target_link_libraries(${TinyOrm_target} PUBLIC tabulate::tabulate) +endif() + # Build auto tests # --- @@ -295,6 +355,13 @@ if(BUILD_TESTS) add_subdirectory(tests) endif() +# Build examples +# --- + +if(TOM_EXAMPLE) + add_subdirectory(examples) +endif() + # Deployment # --- @@ -355,10 +422,20 @@ set_package_properties(range-v3 if(MYSQL_PING) set_package_properties(MySQL PROPERTIES + # URL and DESCRIPTION are already set by Find-module Package (FindMySQL.cmake) TYPE REQUIRED PURPOSE "Provides MySQL ping, enables MySqlConnection::pingDatabase()" ) endif() +if(TOM) + set_package_properties(tabulate + PROPERTIES + URL "https://github.com/p-ranav/tabulate/" + DESCRIPTION "Table Maker for Modern C++" + TYPE REQUIRED + PURPOSE "Used by the tom in the migrate:status command" + ) +endif() if(VERBOSE_CONFIGURE) if(NOT TINY_IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/TinyOrm.pro b/TinyOrm.pro index 80cd39659..119bf0cec 100644 --- a/TinyOrm.pro +++ b/TinyOrm.pro @@ -1,9 +1,16 @@ TEMPLATE = subdirs -SUBDIRS += src +SUBDIRS = src + +tom_example { + SUBDIRS += examples + examples.depends = src +} # Can be enabled by CONFIG += build_tests when the qmake.exe for the project is called build_tests { SUBDIRS += tests tests.depends = src + + !build_pass: message("Build TinyORM unit tests.") } diff --git a/cmake/Modules/TinyInitDefaultVariables.cmake b/cmake/Modules/TinyInitDefaultVariables.cmake index 0258acce6..42c9ff4fb 100644 --- a/cmake/Modules/TinyInitDefaultVariables.cmake +++ b/cmake/Modules/TinyInitDefaultVariables.cmake @@ -143,10 +143,12 @@ macro(tiny_init_tiny_variables_pre) # a main package name set(TinyOrm_ns TinyOrm) set(TinyUtils_ns TinyUtils) + set(TomExample_ns tom) # Target names set(CommonConfig_target CommonConfig) set(TinyOrm_target TinyOrm) set(TinyUtils_target TinyUtils) + set(TomExample_target tom) get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) set(TINY_IS_MULTI_CONFIG "${isMultiConfig}" CACHE INTERNAL diff --git a/cmake/Modules/TinySources.cmake b/cmake/Modules/TinySources.cmake index d1ac0a952..66a6d0267 100644 --- a/cmake/Modules/TinySources.cmake +++ b/cmake/Modules/TinySources.cmake @@ -1,7 +1,8 @@ # TinyORM library header and source files # Create header and source files lists and return them -function(tiny_sources out_headers out_sources) +function(tinyorm_sources out_headers out_sources) + # ORM headers section set(headers) if(TINY_EXTERN_CONSTANTS) @@ -131,10 +132,17 @@ function(tiny_sources out_headers out_sources) tiny/types/connectionoverride.hpp tiny/types/syncchanges.hpp tiny/utils/attribute.hpp + ) + endif() + + # Headers used in both ORM and TOM + if(ORM OR TOM) + list(APPEND headers tiny/utils/string.hpp ) endif() + # ORM sources section set(sources) if(TINY_EXTERN_CONSTANTS) @@ -203,6 +211,12 @@ function(tiny_sources out_headers out_sources) tiny/tinytypes.cpp tiny/types/syncchanges.cpp tiny/utils/attribute.cpp + ) + endif() + + # Sources needed in both ORM and TOM + if(ORM OR TOM) + list(APPEND sources tiny/utils/string.cpp ) endif() @@ -215,5 +229,91 @@ function(tiny_sources out_headers out_sources) set(${out_headers} ${headers} PARENT_SCOPE) set(${out_sources} ${sources} PARENT_SCOPE) - +endfunction() + +# TinyTom application header and source files +# Create header and source files lists and return them +function(tinytom_sources out_headers out_sources) + + # Tom headers section + set(headers) + + list(APPEND headers + application.hpp + commands/command.hpp + commands/database/wipecommand.hpp + commands/environmentcommand.hpp + commands/helpcommand.hpp + commands/inspirecommand.hpp + commands/listcommand.hpp + commands/make/migrationcommand.hpp +# commands/make/projectcommand.hpp + commands/make/stubs/migrationstubs.hpp + commands/make/stubs/projectstubs.hpp + commands/migrations/freshcommand.hpp + commands/migrations/installcommand.hpp + commands/migrations/migratecommand.hpp + commands/migrations/refreshcommand.hpp + commands/migrations/resetcommand.hpp + commands/migrations/rollbackcommand.hpp + commands/migrations/statuscommand.hpp + concerns/callscommands.hpp + concerns/confirmable.hpp + concerns/interactswithio.hpp + concerns/printsoptions.hpp + config.hpp + exceptions/invalidargumenterror.hpp + exceptions/invalidtemplateargumenterror.hpp + exceptions/logicerror.hpp + exceptions/runtimeerror.hpp + exceptions/tomerror.hpp + migration.hpp + migrationcreator.hpp + migrationrepository.hpp + migrator.hpp + terminal.hpp + tomtypes.hpp + version.hpp + ) + + # Tom sources section + set(sources) + + list(APPEND sources + application.cpp + commands/command.cpp + commands/database/wipecommand.cpp + commands/environmentcommand.cpp + commands/helpcommand.cpp + commands/inspirecommand.cpp + commands/listcommand.cpp + commands/make/migrationcommand.cpp +# commands/make/projectcommand.cpp + commands/migrations/freshcommand.cpp + commands/migrations/installcommand.cpp + commands/migrations/migratecommand.cpp + commands/migrations/refreshcommand.cpp + commands/migrations/resetcommand.cpp + commands/migrations/rollbackcommand.cpp + commands/migrations/statuscommand.cpp + concerns/callscommands.cpp + concerns/confirmable.cpp + concerns/interactswithio.cpp + concerns/printsoptions.cpp + exceptions/tomlogicerror.cpp + exceptions/tomruntimeerror.cpp + migrationcreator.cpp + migrationrepository.cpp + migrator.cpp + terminal.cpp + ) + + list(SORT headers) + list(SORT sources) + + list(TRANSFORM headers PREPEND "${CMAKE_SOURCE_DIR}/tom/include/tom/") + list(TRANSFORM sources PREPEND "${CMAKE_SOURCE_DIR}/tom/src/tom/") + + set(${out_headers} ${headers} PARENT_SCOPE) + set(${out_sources} ${sources} PARENT_SCOPE) endfunction() diff --git a/cmake/Modules/TinyTestCommon.cmake b/cmake/Modules/TinyTestCommon.cmake index 6a5c6c7ab..34ab175fa 100644 --- a/cmake/Modules/TinyTestCommon.cmake +++ b/cmake/Modules/TinyTestCommon.cmake @@ -3,7 +3,7 @@ include(TinyResourceAndManifest) # Configure passed auto test function(tiny_configure_test name) - set(options INCLUDE_MODELS) + set(options INCLUDE_MIGRATIONS INCLUDE_MODELS) cmake_parse_arguments(PARSE_ARGV 1 TINY "${options}" "" "") if(DEFINED TINY_UNPARSED_ARGUMENTS) @@ -54,6 +54,12 @@ ${TINY_UNPARSED_ARGUMENTS}") "$" ) + if(TINY_INCLUDE_MIGRATIONS) + target_include_directories(${name} PRIVATE + "$" + ) + endif() + if(TINY_INCLUDE_MODELS) target_include_directories(${name} PRIVATE "$" @@ -62,8 +68,6 @@ ${TINY_UNPARSED_ARGUMENTS}") target_link_libraries(${name} PRIVATE - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Sql Qt${QT_VERSION_MAJOR}::Test ${TinyOrm_ns}::${TinyUtils_target} ${TinyOrm_ns}::${TinyOrm_target} diff --git a/cmake/vcpkg/ports/tinyorm/vcpkg.json b/cmake/vcpkg/ports/tinyorm/vcpkg.json index 783414eac..1220d9e3d 100644 --- a/cmake/vcpkg/ports/tinyorm/vcpkg.json +++ b/cmake/vcpkg/ports/tinyorm/vcpkg.json @@ -8,6 +8,7 @@ "supports": "!(uwp | arm | android | emscripten)", "dependencies": [ "range-v3", + "tabulate", "qt5-base", { "name": "vcpkg-cmake", diff --git a/conf.pri.example b/conf.pri.example index 580623c5c..aca86c4e1 100644 --- a/conf.pri.example +++ b/conf.pri.example @@ -21,6 +21,11 @@ win32-g++|win32-clang-g++ { # Enable ccache wrapper CONFIG *= tiny_ccache + # Includes + # tabulate + INCLUDEPATH += $$quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + QMAKE_CXXFLAGS += -isystem $$shell_quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + # Libraries # MySQL C library # Find with the pkg-config (preferred), shared build only @@ -64,7 +69,7 @@ else:win32-msvc { else:unix { # Includes # range-v3 - QMAKE_CXXFLAGS += -isystem $$quote(/home/xyz/vcpkg/installed/x64-linux/include/) + QMAKE_CXXFLAGS += -isystem $$shell_quote(/home/xyz/vcpkg/installed/x64-linux/include/) # Libraries # MySQL C library diff --git a/docs/building.mdx b/docs/building.mdx index b03caf4d6..da514bf12 100644 --- a/docs/building.mdx +++ b/docs/building.mdx @@ -617,7 +617,7 @@ TINY_TINYORM_BUILDS_DIR = $$quote($$TINY_MAIN_DIR/TinyOrm-builds-qmake) include($$TINY_MAIN_DIR/TinyORM/qmake/TinyOrm.pri) # TinyORM header files -QMAKE_CXXFLAGS += -isystem $$quote($$TINY_MAIN_DIR/TinyORM/include/) +QMAKE_CXXFLAGS += -isystem $$shell_quote($$TINY_MAIN_DIR/TinyORM/include/) # TinyORM library path LIBS += $$quote(-L$$TINY_TINYORM_BUILDS_DIR/build-TinyOrm-Desktop_Qt_5_15_2_GCC_64bit-Debug/src/debug/) @@ -625,7 +625,7 @@ LIBS += -lTinyOrm # vcpkg - range-v3 # --- -QMAKE_CXXFLAGS += -isystem $$quote(../../../../vcpkg/installed/x64-linux/include/) +QMAKE_CXXFLAGS += -isystem $$shell_quote(../../../../vcpkg/installed/x64-linux/include/) ``` diff --git a/docs/hello-world.mdx b/docs/hello-world.mdx index 06864fe6c..ed296056c 100644 --- a/docs/hello-world.mdx +++ b/docs/hello-world.mdx @@ -456,7 +456,7 @@ TINY_TINYORM_BUILDS_DIR = $$quote($$TINY_MAIN_DIR/TinyOrm-builds-qmake) include($$TINY_MAIN_DIR/TinyORM/qmake/TinyOrm.pri) # TinyORM header files -QMAKE_CXXFLAGS += -isystem $$quote($$TINY_MAIN_DIR/TinyORM/include/) +QMAKE_CXXFLAGS += -isystem $$shell_quote($$TINY_MAIN_DIR/TinyORM/include/) # TinyORM library path LIBS += $$quote(-L$$TINY_TINYORM_BUILDS_DIR/build-TinyOrm-Desktop_Qt_5_15_2_MSVC2019_64bit-Debug/src/debug/) @@ -464,7 +464,7 @@ LIBS += -lTinyOrm # vcpkg - range-v3 # --- -QMAKE_CXXFLAGS += -isystem $$quote(../../../../vcpkg/installed/x64-linux/include/) +QMAKE_CXXFLAGS += -isystem $$shell_quote(../../../../vcpkg/installed/x64-linux/include/) ``` :::tip diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 000000000..f477733f0 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,3 @@ +if(TOM_EXAMPLE) + add_subdirectory(tom) +endif() diff --git a/examples/examples.pro b/examples/examples.pro new file mode 100644 index 000000000..8c6c26621 --- /dev/null +++ b/examples/examples.pro @@ -0,0 +1,7 @@ +TEMPLATE = subdirs + +tom_example:!disable_tom { + SUBDIRS += tom + + !build_pass: message("Build the tom example.") +} diff --git a/examples/tom/CMakeLists.txt b/examples/tom/CMakeLists.txt new file mode 100644 index 000000000..d38d60307 --- /dev/null +++ b/examples/tom/CMakeLists.txt @@ -0,0 +1,74 @@ +# Tom command-line application example +# --- + +# Initialize Project Version +# --- + +include(TinyHelpers) +tiny_read_version(TINY_VERSION + TINY_VERSION_MAJOR TINY_VERSION_MINOR TINY_VERSION_PATCH TINY_VERSION_TWEAK + VERSION_HEADER "${CMAKE_SOURCE_DIR}/tom/include/tom/version.hpp" + PREFIX TINYTOM + HEADER_FOR "${TomExample_ns}" +) + +# Basic project +# --- + +project(${TomExample_ns} + DESCRIPTION "Tom console for TinyORM" + HOMEPAGE_URL "https://silverqx.github.io/TinyORM/" + LANGUAGES CXX + VERSION ${TINY_VERSION} +) + +# Tom command-line application +# --- + +add_executable(${TomExample_target} + main.cpp +) +add_executable(${TomExample_ns}::${TomExample_target} ALIAS ${TomExample_target}) + +# Tom command-line application specific configuration +# --- + +set_target_properties(${TomExample_target} + PROPERTIES + C_VISIBILITY_PRESET "hidden" + CXX_VISIBILITY_PRESET "hidden" + VISIBILITY_INLINES_HIDDEN YES + VERSION ${PROJECT_VERSION} +) + +target_include_directories(${TomExample_target} PRIVATE + "$" +) + +# Tom command-line application defines +# --- + +target_compile_definitions(${TomExample_target} + PRIVATE + PROJECT_TOMEXAMPLE +) + +# Windows resource and manifest files +# --- + +# Find icons, tom/version.hpp, and Windows manifest file for MinGW +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + tiny_set_rc_flags("-I \"${CMAKE_SOURCE_DIR}/tom/resources\"") +endif() + +include(TinyResourceAndManifest) +tiny_resource_and_manifest(${TomExample_target} + OUTPUT_DIR "${TINY_BUILD_GENDIR}/tmp/" + RESOURCES_DIR "${CMAKE_SOURCE_DIR}/tom/resources" +) + +# Resolve and link dependencies +# --- + +# Unconditional dependencies +target_link_libraries(${TomExample_target} PRIVATE ${TinyOrm_ns}::${TinyOrm_target}) diff --git a/examples/tom/conf.pri.example b/examples/tom/conf.pri.example new file mode 100644 index 000000000..3e6133165 --- /dev/null +++ b/examples/tom/conf.pri.example @@ -0,0 +1,39 @@ +# Migrations header files +# --- + +# Tests' migrations as example migrations +include($$TINYORM_SOURCE_TREE/tests/database/migrations.pri) +# Or include yours migrations +#include(/home/xyz/your_project/database/migrations.pri) + +# Dependencies include and library paths +# --- + +# MinGW +win32-g++|win32-clang-g++ { + # Enable ccache wrapper + CONFIG *= tiny_ccache + + # Includes + # tabulate + INCLUDEPATH += $$quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + QMAKE_CXXFLAGS += -isystem $$shell_quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + + # Use faster linker + # CONFIG *= use_lld_linker does not work on MinGW + QMAKE_LFLAGS *= -fuse-ld=lld +} +else:win32-msvc { + # Includes + # range-v3 and tabulate + INCLUDEPATH += $$quote(E:/xyz/vcpkg/installed/x64-windows/include/) +} +else:unix { + # Includes + # range-v3 and tabulate + QMAKE_CXXFLAGS += -isystem $$shell_quote(/home/xyz/vcpkg/installed/x64-linux/include/) + + # Use faster linkers + clang: CONFIG *= use_lld_linker + else: CONFIG *= use_gold_linker +} diff --git a/examples/tom/main.cpp b/examples/tom/main.cpp new file mode 100644 index 000000000..d2b26913e --- /dev/null +++ b/examples/tom/main.cpp @@ -0,0 +1,99 @@ +#include + +#include + +#include "migrations/2014_10_12_000000_create_posts_table.hpp" +#include "migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp" +#include "migrations/2014_10_12_200000_create_properties_table.hpp" +#include "migrations/2014_10_12_300000_create_phones_table.hpp" + +using Orm::Constants::H127001; +using Orm::Constants::P3306; +using Orm::Constants::QMYSQL; +using Orm::Constants::SYSTEM; +using Orm::Constants::UTF8MB4; +using Orm::Constants::charset_; +using Orm::Constants::collation_; +using Orm::Constants::database_; +using Orm::Constants::driver_; +using Orm::Constants::engine_; +using Orm::Constants::host_; +using Orm::Constants::InnoDB; +using Orm::Constants::isolation_level; +using Orm::Constants::options_; +using Orm::Constants::password_; +using Orm::Constants::port_; +using Orm::Constants::prefix_; +using Orm::Constants::prefix_indexes; +using Orm::Constants::strict_; +using Orm::Constants::timezone_; +using Orm::Constants::username_; + +using Orm::DatabaseManager; +using Orm::DB; + +using TomApplication = Tom::Application; + +using namespace Migrations; // NOLINT(google-build-using-namespace) + +/*! Build the database manager instance and add a database connection. */ +std::shared_ptr setupManager(); + +/*! c++ main function. */ +int main(int argc, char *argv[]) +{ + try { + // Ownership of the shared_ptr() + auto db = setupManager(); + + return TomApplication(argc, argv, db, "TOM_EXAMPLE_ENV") + .migrations< + _2014_10_12_000000_create_posts_table, + _2014_10_12_100000_add_factor_column_to_posts_table, + _2014_10_12_200000_create_properties_table, + _2014_10_12_300000_create_phones_table>() + // Fire it up 🔥🚀✨ + .run(); + + } catch (const std::exception &e) { + + TomApplication::logException(e); + } + + return EXIT_FAILURE; +} + +std::shared_ptr setupManager() +{ + // Ownership of the shared_ptr() + return DB::create({ + {driver_, QMYSQL}, + {host_, qEnvironmentVariable("DB_MYSQL_HOST", H127001)}, + {port_, qEnvironmentVariable("DB_MYSQL_PORT", P3306)}, + {database_, qEnvironmentVariable("DB_MYSQL_DATABASE", "")}, + {username_, qEnvironmentVariable("DB_MYSQL_USERNAME", "")}, + {password_, qEnvironmentVariable("DB_MYSQL_PASSWORD", "")}, + {charset_, qEnvironmentVariable("DB_MYSQL_CHARSET", UTF8MB4)}, + {collation_, qEnvironmentVariable("DB_MYSQL_COLLATION", + QStringLiteral("utf8mb4_0900_ai_ci"))}, + {timezone_, SYSTEM}, + {prefix_, ""}, + {prefix_indexes, true}, + {strict_, true}, + {isolation_level, QStringLiteral("REPEATABLE READ")}, + {engine_, InnoDB}, + {options_, QVariantHash()}, + }, + QLatin1String("tinyorm_tom")); +} + +/* Alternative syntax to instantiate migration classes. */ +// return TomApplication(argc, argv, db, "TOM_EXAMPLE_ENV", +// { +// std::make_shared<_2014_10_12_000000_create_posts_table>(), +// std::make_shared<_2014_10_12_100000_add_factor_column_to_posts_table>(), +// std::make_shared<_2014_10_12_200000_create_properties_table>(), +// std::make_shared<_2014_10_12_300000_create_phones_table>(), +// }) +// // Fire it up 🔥🚀✨ +// .run(); diff --git a/examples/tom/tom.pro b/examples/tom/tom.pro new file mode 100644 index 000000000..aedd0acde --- /dev/null +++ b/examples/tom/tom.pro @@ -0,0 +1,53 @@ +QT *= core sql +QT -= gui + +TEMPLATE = app +TARGET = tom + +# TinyTom example application specific configuration +# --- + +CONFIG *= console + +include($$TINYORM_SOURCE_TREE/qmake/tom.pri) + +# TinyTom example application defines +# --- + +DEFINES += PROJECT_TOMEXAMPLE + +# TinyTom defines +# --- +# this define is not provided in the qmake/tom.pri + +# Enable code needed by tests (modify the migrate:status command for tests need) +build_tests: \ + DEFINES *= TINYTOM_TESTS_CODE + +# TinyTom example application header and source files +# --- + +SOURCES += $$PWD/main.cpp + +# Deployment +# --- + +win32-msvc:CONFIG(debug, debug|release) { + win32-msvc: target.path = C:/optx64/$${TARGET} +# else: unix:!android: target.path = /opt/$${TARGET}/bin + !isEmpty(target.path): INSTALLS += target +} + +# User Configuration +# --- + +exists(conf.pri): \ + include(conf.pri) + +#else:is_vcpkg_build: \ +# include(../qmake/vcpkgconf.pri) + +else: \ + error( "'conf.pri' for 'tom' example project does not exist. See an example\ + configuration in 'examples/tom/conf.pri.example' or call 'vcpkg install'\ + in the project's root." ) diff --git a/include/include.pri b/include/include.pri index c0cc97c6b..5d7a9a4dd 100644 --- a/include/include.pri +++ b/include/include.pri @@ -126,6 +126,9 @@ headersList += \ $$PWD/orm/tiny/types/connectionoverride.hpp \ $$PWD/orm/tiny/types/syncchanges.hpp \ $$PWD/orm/tiny/utils/attribute.hpp \ + +!disable_orm|!disable_tom: \ + headersList += \ $$PWD/orm/tiny/utils/string.hpp \ HEADERS += $$sorted(headersList) diff --git a/include/orm/databaseconnection.hpp b/include/orm/databaseconnection.hpp index f5985583e..5038948b6 100644 --- a/include/orm/databaseconnection.hpp +++ b/include/orm/databaseconnection.hpp @@ -179,7 +179,7 @@ namespace SchemaNs /*! Get the query grammar used by the connection. */ inline QueryGrammar &getQueryGrammar(); /*! Get the schema grammar used by the connection. */ - inline const SchemaGrammar &getSchemaGrammar() const; + const SchemaGrammar &getSchemaGrammar(); /*! Get a schema builder instance for the connection. */ virtual std::unique_ptr getSchemaBuilder(); /*! Get the query post processor used by the connection. */ @@ -401,11 +401,6 @@ namespace SchemaNs return *m_queryGrammar; } - const SchemaGrammar &DatabaseConnection::getSchemaGrammar() const - { - return *m_schemaGrammar; - } - const QueryProcessor &DatabaseConnection::getPostProcessor() const { return *m_postProcessor; diff --git a/include/orm/exceptions/domainerror.hpp b/include/orm/exceptions/domainerror.hpp index 1dcc40e47..247222bcc 100644 --- a/include/orm/exceptions/domainerror.hpp +++ b/include/orm/exceptions/domainerror.hpp @@ -5,8 +5,6 @@ #include "orm/macros/systemheader.hpp" TINY_SYSTEM_HEADER -#include - #include "orm/exceptions/logicerror.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -14,7 +12,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Domain exception. */ + /*! TinyORM Domain exception. */ class DomainError : public LogicError { /*! Inherit constructors. */ diff --git a/include/orm/exceptions/invalidargumenterror.hpp b/include/orm/exceptions/invalidargumenterror.hpp index 231511ee9..61c3515c9 100644 --- a/include/orm/exceptions/invalidargumenterror.hpp +++ b/include/orm/exceptions/invalidargumenterror.hpp @@ -5,8 +5,6 @@ #include "orm/macros/systemheader.hpp" TINY_SYSTEM_HEADER -#include - #include "orm/exceptions/logicerror.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -14,7 +12,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Invalid argument exception. */ + /*! TinyORM invalid argument exception. */ class InvalidArgumentError : public LogicError { /*! Inherit constructors. */ diff --git a/include/orm/exceptions/invalidformaterror.hpp b/include/orm/exceptions/invalidformaterror.hpp index 703e136b4..dad6ad2da 100644 --- a/include/orm/exceptions/invalidformaterror.hpp +++ b/include/orm/exceptions/invalidformaterror.hpp @@ -12,7 +12,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Invalid format exception. */ + /*! TinyORM invalid format exception. */ class InvalidFormatError : public LogicError { /*! Inherit constructors. */ diff --git a/include/orm/exceptions/invalidtemplateargumenterror.hpp b/include/orm/exceptions/invalidtemplateargumenterror.hpp index 8f7a7d325..63cd5d588 100644 --- a/include/orm/exceptions/invalidtemplateargumenterror.hpp +++ b/include/orm/exceptions/invalidtemplateargumenterror.hpp @@ -12,7 +12,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Invalid template argument exception. */ + /*! TinyORM invalid template argument exception. */ class InvalidTemplateArgumentError : public InvalidArgumentError { /*! Inherit constructors. */ diff --git a/include/orm/exceptions/logicerror.hpp b/include/orm/exceptions/logicerror.hpp index b6469cae4..f032684af 100644 --- a/include/orm/exceptions/logicerror.hpp +++ b/include/orm/exceptions/logicerror.hpp @@ -10,7 +10,6 @@ TINY_SYSTEM_HEADER #include #include "orm/exceptions/ormerror.hpp" -#include "orm/macros/commonnamespace.hpp" #include "orm/macros/export.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -18,7 +17,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Logic exception. */ + /*! TinyORM Logic exception. */ class SHAREDLIB_EXPORT LogicError : public std::logic_error, public OrmError @@ -28,16 +27,18 @@ namespace Orm::Exceptions explicit LogicError(const char *message); /*! QString constructor. */ explicit LogicError(const QString &message); + /*! std::string constructor. */ + explicit LogicError(const std::string &message); /*! Return exception message as a QString. */ - const QString &message() const; + inline const QString &message() const noexcept; protected: /*! Exception message. */ QString m_message = what(); }; - inline const QString &LogicError::message() const + const QString &LogicError::message() const noexcept { return m_message; } diff --git a/include/orm/exceptions/queryerror.hpp b/include/orm/exceptions/queryerror.hpp index d8c1c9ba9..1b169ef2b 100644 --- a/include/orm/exceptions/queryerror.hpp +++ b/include/orm/exceptions/queryerror.hpp @@ -16,7 +16,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Database query exception. */ + /*! TinyORM Database query exception. */ class SHAREDLIB_EXPORT QueryError : public SqlError { public: diff --git a/include/orm/exceptions/runtimeerror.hpp b/include/orm/exceptions/runtimeerror.hpp index 1d2b1e67b..5942de812 100644 --- a/include/orm/exceptions/runtimeerror.hpp +++ b/include/orm/exceptions/runtimeerror.hpp @@ -10,7 +10,6 @@ TINY_SYSTEM_HEADER #include #include "orm/exceptions/ormerror.hpp" -#include "orm/macros/commonnamespace.hpp" #include "orm/macros/export.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -18,7 +17,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Runtime exception. */ + /*! TinyORM Runtime exception. */ class SHAREDLIB_EXPORT RuntimeError : public std::runtime_error, public OrmError @@ -28,16 +27,18 @@ namespace Orm::Exceptions explicit RuntimeError(const char *message); /*! QString constructor. */ explicit RuntimeError(const QString &message); + /*! std::string constructor. */ + explicit RuntimeError(const std::string &message); /*! Return exception message as a QString. */ - const QString &message() const; + inline const QString &message() const noexcept; protected: /*! Exception message. */ QString m_message = what(); }; - inline const QString &RuntimeError::message() const + const QString &RuntimeError::message() const noexcept { return m_message; } diff --git a/include/orm/exceptions/sqlerror.hpp b/include/orm/exceptions/sqlerror.hpp index 8c4e665b6..73b267679 100644 --- a/include/orm/exceptions/sqlerror.hpp +++ b/include/orm/exceptions/sqlerror.hpp @@ -14,7 +14,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! SqlError exception, wrapper for the QSqlError. */ + /*! TinyORM SqlError exception, wrapper for the QSqlError. */ class SHAREDLIB_EXPORT SqlError : public RuntimeError { public: @@ -24,7 +24,7 @@ namespace Orm::Exceptions SqlError(const QString &message, const QSqlError &error); /*! Get the original Qt SQL error. */ - const QSqlError &getSqlError() const; + inline const QSqlError &getSqlError() const noexcept; protected: /*! Internal ctor for use from descendants to avoid an error message @@ -38,6 +38,11 @@ namespace Orm::Exceptions QSqlError m_sqlError; }; + const QSqlError &SqlError::getSqlError() const noexcept + { + return m_sqlError; + } + } // namespace Orm::Exceptions TINYORM_END_COMMON_NAMESPACE diff --git a/include/orm/exceptions/sqltransactionerror.hpp b/include/orm/exceptions/sqltransactionerror.hpp index df1325301..86660d5ce 100644 --- a/include/orm/exceptions/sqltransactionerror.hpp +++ b/include/orm/exceptions/sqltransactionerror.hpp @@ -12,7 +12,7 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Exceptions { - /*! Sql transaction exception. */ + /*! TinyORM Sql transaction exception. */ class SqlTransactionError : public SqlError { /*! Inherit constructors. */ diff --git a/include/orm/macros/commonnamespace.hpp b/include/orm/macros/commonnamespace.hpp index e7becab08..6949915a5 100644 --- a/include/orm/macros/commonnamespace.hpp +++ b/include/orm/macros/commonnamespace.hpp @@ -10,6 +10,7 @@ TINY_SYSTEM_HEADER # define TINYORM_BEGIN_COMMON_NAMESPACE namespace TINYORM_COMMON_NAMESPACE { # define TINYORM_END_COMMON_NAMESPACE } #else +# define TINYORM_COMMON_NAMESPACE # define TINYORM_BEGIN_COMMON_NAMESPACE # define TINYORM_END_COMMON_NAMESPACE #endif diff --git a/include/orm/schema/schemabuilder.hpp b/include/orm/schema/schemabuilder.hpp index deef97b11..e2febdc52 100644 --- a/include/orm/schema/schemabuilder.hpp +++ b/include/orm/schema/schemabuilder.hpp @@ -9,7 +9,7 @@ TINY_SYSTEM_HEADER #include "orm/macros/commonnamespace.hpp" #include "orm/macros/export.hpp" -// CUR check this on clean project silverqx +// CUR tom, check this on clean project silverqx // Include the blueprint here so a user doesn't have to #include "orm/schema/blueprint.hpp" diff --git a/include/orm/tiny/concerns/hasattributes.hpp b/include/orm/tiny/concerns/hasattributes.hpp index 8b4c5c48c..c95065645 100644 --- a/include/orm/tiny/concerns/hasattributes.hpp +++ b/include/orm/tiny/concerns/hasattributes.hpp @@ -16,17 +16,19 @@ TINY_SYSTEM_HEADER TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::Tiny -{ - namespace TinyUtils = Orm::Tiny::Utils; - -namespace Concerns +namespace Orm::Tiny::Concerns { /*! Model attributes. */ template class HasAttributes { + // CUR utils, use this using pattern also for all Orm::Utils silverqx + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; + /*! Alias for the string utils. */ + using StringUtils = Orm::Tiny::Utils::String; + public: /*! Set a given attribute on the model. */ Derived &setAttribute(const QString &key, QVariant value); @@ -258,7 +260,7 @@ namespace Concerns const bool sync) { m_attributes.reserve(attributes.size()); - m_attributes = TinyUtils::Attribute::removeDuplicitKeys(attributes); + m_attributes = AttributeUtils::removeDuplicitKeys(attributes); // Build attributes hash m_attributesHash.clear(); @@ -831,7 +833,7 @@ namespace Concerns and format a Carbon object from this timestamp. This allows flexibility when defining your date fields as they might be UNIX timestamps here. */ if (value.canConvert() && - TinyUtils::String::isNumber(value.value()) + StringUtils::isNumber(value.value()) ) // TODO switch ms accuracy? For the u_dateFormat too? silverqx return QDateTime::fromSecsSinceEpoch(value.value()); @@ -884,8 +886,7 @@ namespace Concerns /* Static cast this to a child's instance type (CRTP) */ TINY_CRTP_MODEL_WITH_BASE_DEFINITIONS(HasAttributes) -} // namespace Concerns -} // namespace Orm::Tiny +} // namespace Orm::Tiny::Concerns TINYORM_END_COMMON_NAMESPACE diff --git a/include/orm/tiny/concerns/hasrelationships.hpp b/include/orm/tiny/concerns/hasrelationships.hpp index efd3a88b8..3569f95e7 100644 --- a/include/orm/tiny/concerns/hasrelationships.hpp +++ b/include/orm/tiny/concerns/hasrelationships.hpp @@ -6,9 +6,9 @@ TINY_SYSTEM_HEADER #ifdef __GNUG__ -#include +# include #else -#include +# include #endif #include @@ -955,7 +955,6 @@ namespace Concerns QString HasRelationships::guessBelongsToRelationInternal() const { - // TODO reliability, also add Orm::Tiny::Utils::String::studly silverqx auto relation = Orm::Utils::Type::classPureBasename(); relation[0] = relation[0].toLower(); diff --git a/include/orm/tiny/model.hpp b/include/orm/tiny/model.hpp index aa4f452da..c8b357288 100644 --- a/include/orm/tiny/model.hpp +++ b/include/orm/tiny/model.hpp @@ -16,7 +16,7 @@ TINY_SYSTEM_HEADER #include "orm/tiny/modelproxies.hpp" #include "orm/tiny/tinybuilder.hpp" #ifdef TINYORM_TESTS_CODE -#include "orm/tiny/types/connectionoverride.hpp" +# include "orm/tiny/types/connectionoverride.hpp" #endif TINYORM_BEGIN_COMMON_NAMESPACE @@ -63,6 +63,10 @@ namespace Orm::Tiny // Used by TinyBuilder::eagerLoadRelations() friend TinyBuilder; + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; + /*! Alias for the string utils. */ + using StringUtils = Orm::Tiny::Utils::String; /*! Apply all the Model's template parameters to the passed T template argument. */ template typename T> @@ -893,8 +897,7 @@ namespace Orm::Tiny if (table.isEmpty()) const_cast(model().u_table) = QStringLiteral("%1s").arg( - TinyUtils::String::toSnake( - Orm::Utils::Type::classPureBasename())); + StringUtils::snake(Orm::Utils::Type::classPureBasename())); return table; } @@ -940,8 +943,7 @@ namespace Orm::Tiny QString Model::getForeignKey() const { return QStringLiteral("%1_%2").arg( - TinyUtils::String::toSnake( - Orm::Utils::Type::classPureBasename()), + StringUtils::snake(Orm::Utils::Type::classPureBasename()), getKeyName()); } @@ -1076,7 +1078,7 @@ namespace Orm::Tiny if (!dirty.isEmpty()) { model().setKeysForSaveQuery(query).update( - TinyUtils::Attribute::convertVectorToUpdateItem(dirty)); + AttributeUtils::convertVectorToUpdateItem(dirty)); this->syncChanges(); @@ -1327,3 +1329,22 @@ TINYORM_END_COMMON_NAMESPACE // CUR model, add whereBelongsTo, whereRelation, orWhereRelation silverqx // CUR schema, add tests for enum and set; and json and jsonb, storedAs / virtualAs silverqx // CUR propagation, https://ben.balter.com/2017/11/10/twelve-tips-for-growing-communities-around-your-open-source-project/ silverqx +// CUR optimization, use Q_UNREACHABLE in all switch statements, of course where appropriate silverqx +// TODO vcpkg, solve how to build tom (when solving vcpkg builds again), currently I have hardly added tabulate to the vcpkg.json port and also manifest file; it will have to be conditional base of the TINYORM_DISABLE_TOM macro silverqx +// CUR tom docs, disable_tom and TINYORM_DISABLE_TOM to build.mdx, don't forget to add features and update dependencies (tabulate) in vcpkg.json silverqx +// CUR schema, add tests for enum and set; and json and jsonb, storedAs / virtualAs silverqx +// CUR compiler, enable /sdl on msvc https://docs.microsoft.com/en-us/cpp/build/reference/sdl-enable-additional-security-checks?view=msvc-170 silverqx +// CUR cmake, update max. policy to 3.23 silverqx +// CUR tom, add tabulate to comments where range-v3 is, all checked, only docs left silverqx +// CUR tom, verify -isystem $$shell_quote() updated in docs silverqx +// CUR tom, tom/conf.pri is used by who silverqx +// CUR cmake, add messages about Building tom example, tests and ORM silverqx +// CUR docs, remove target_link_libs() for transitive dependencies silverqx +// CUR tests, move version test outside of the orm/ folder silverqx +// CUR tom docs, write documentation silverqx +// CUR tom, build on mingw, linux, build without pch and all common tasks that should run from time to time silverqx +// CUR tom, update docs target_link_library() https://discourse.cmake.org/t/explicitly-link-against-public-interface-dependencies/5484/2 silverqx +// CUR tom, provide somehow a custom path to migrations for the tom example, so the tom example can be used like real migration app silverqx +// CUR tom, don't modify migrate:status command, rather extend it and add possibility to only call it through Application::runWithArguments() (this secure to not be able to call it from the cmd. line), do not show it in the list or help command output silverqx +// CUR tom, think about remove TINYTOM_NO/_DEBUG and TINYTOM_TESTS_CODE and use TINYORM_ defines instead silverqx +// BUG rc file © encoding silverqx diff --git a/include/orm/tiny/relations/basepivot.hpp b/include/orm/tiny/relations/basepivot.hpp index 40f46c094..3e249f066 100644 --- a/include/orm/tiny/relations/basepivot.hpp +++ b/include/orm/tiny/relations/basepivot.hpp @@ -20,6 +20,9 @@ namespace Orm::Tiny::Relations template class BasePivot : public Model, public IsPivotModel { + /*! Alias for the string utils. */ + using StringUtils = Orm::Tiny::Utils::String; + public: friend Model; @@ -234,8 +237,8 @@ namespace Orm::Tiny::Relations // Get singularizes snake-case table name if (table.isEmpty()) - return TinyUtils::String::singular( - TinyUtils::String::toSnake( + return StringUtils::singular( + StringUtils::snake( Orm::Utils::Type::classPureBasename())); return table; diff --git a/include/orm/tiny/relations/belongstomany.hpp b/include/orm/tiny/relations/belongstomany.hpp index 1815e1d4e..d666eab95 100644 --- a/include/orm/tiny/relations/belongstomany.hpp +++ b/include/orm/tiny/relations/belongstomany.hpp @@ -25,8 +25,6 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Tiny::Relations { - namespace TinyUtils = Orm::Tiny::Utils; - class Pivot; /*! TinyORM's 'Pivot' class. */ @@ -46,6 +44,8 @@ namespace Orm::Tiny::Relations { Q_DISABLE_COPY(BelongsToMany) + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; /*! Model alias. */ template using BaseModel = Orm::Tiny::Model; @@ -953,7 +953,7 @@ namespace Orm::Tiny::Relations return *instance; return this->m_related->newInstance( - TinyUtils::Attribute::joinAttributesForFirstOr( + AttributeUtils::joinAttributesForFirstOr( attributes, values, this->m_relatedKey)); } @@ -969,7 +969,7 @@ namespace Orm::Tiny::Relations return *instance; // NOTE api different, Eloquent doen't use values argument silverqx - return create(TinyUtils::Attribute::joinAttributesForFirstOr( + return create(AttributeUtils::joinAttributesForFirstOr( attributes, values, this->m_relatedKey), pivotValues, touch); } @@ -1154,7 +1154,7 @@ namespace Orm::Tiny::Relations we have inserted the records, we will touch the relationships if necessary and the function will return. */ newPivotStatement()->insert( - TinyUtils::Attribute::convertVectorsToMaps( + AttributeUtils::convertVectorsToMaps( formatAttachRecords(ids, attributes))); else attachUsingCustomClass(ids, attributes); @@ -1200,7 +1200,7 @@ namespace Orm::Tiny::Relations we have inserted the records, we will touch the relationships if necessary and the function will return. */ newPivotStatement()->insert( - TinyUtils::Attribute::convertVectorsToMaps( + AttributeUtils::convertVectorsToMaps( formatAttachRecords(idsWithAttributes))); else attachUsingCustomClass(idsWithAttributes); @@ -1334,7 +1334,7 @@ namespace Orm::Tiny::Relations int updated = -1; std::tie(updated, std::ignore) = newPivotStatementForId(id)->update( - TinyUtils::Attribute::convertVectorToUpdateItem( + AttributeUtils::convertVectorToUpdateItem( castAttributes(attributes))); /* It will not touch if attributes size is 0, because this function is called diff --git a/include/orm/tiny/relations/hasoneormany.hpp b/include/orm/tiny/relations/hasoneormany.hpp index 9422bb3b0..e13335c5e 100644 --- a/include/orm/tiny/relations/hasoneormany.hpp +++ b/include/orm/tiny/relations/hasoneormany.hpp @@ -12,7 +12,6 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Tiny::Relations { - namespace TinyUtils = Orm::Tiny::Utils; /*! Has one/many relation base class. */ template @@ -20,6 +19,9 @@ namespace Orm::Tiny::Relations { Q_DISABLE_COPY(HasOneOrMany) + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; + protected: /*! Protected constructor. */ HasOneOrMany(std::unique_ptr &&related, Model &parent, @@ -199,7 +201,7 @@ namespace Orm::Tiny::Relations auto newInstance = this->m_related->newInstance( - TinyUtils::Attribute::joinAttributesForFirstOr( + AttributeUtils::joinAttributesForFirstOr( attributes, values, this->m_relatedKey)); setForeignAttributesForCreate(newInstance); @@ -218,7 +220,7 @@ namespace Orm::Tiny::Relations if (instance) return *instance; - return create(TinyUtils::Attribute::joinAttributesForFirstOr( + return create(AttributeUtils::joinAttributesForFirstOr( attributes, values, this->m_relatedKey)); } diff --git a/include/orm/tiny/tinybuilder.hpp b/include/orm/tiny/tinybuilder.hpp index c53ccf623..8e105fd8e 100644 --- a/include/orm/tiny/tinybuilder.hpp +++ b/include/orm/tiny/tinybuilder.hpp @@ -32,6 +32,9 @@ namespace Orm::Tiny // Used by TinyBuilderProxies::where/latest/oldest/update() friend BuilderProxies; + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; + public: /*! Constructor. */ Builder(const QSharedPointer &query, Model &model); @@ -348,9 +351,8 @@ namespace Orm::Tiny if (instance) return *instance; - return newModelInstance( - TinyUtils::Attribute::joinAttributesForFirstOr( - attributes, values, m_model.getKeyName())); + return newModelInstance(AttributeUtils::joinAttributesForFirstOr( + attributes, values, m_model.getKeyName())); } template @@ -363,9 +365,8 @@ namespace Orm::Tiny return *instance; auto newInstance = - newModelInstance( - TinyUtils::Attribute::joinAttributesForFirstOr( - attributes, values, m_model.getKeyName())); + newModelInstance(AttributeUtils::joinAttributesForFirstOr( + attributes, values, m_model.getKeyName())); newInstance.save(); diff --git a/include/orm/tiny/tinybuilderproxies.hpp b/include/orm/tiny/tinybuilderproxies.hpp index 41ef2ed93..3534d8185 100644 --- a/include/orm/tiny/tinybuilderproxies.hpp +++ b/include/orm/tiny/tinybuilderproxies.hpp @@ -23,7 +23,6 @@ namespace Query namespace Tiny { - namespace TinyUtils = Orm::Tiny::Utils; /*! Contains proxy methods to the QueryBuilder. */ template @@ -31,6 +30,8 @@ namespace Tiny { Q_DISABLE_COPY(BuilderProxies) + /*! Alias for the attribute utils. */ + using AttributeUtils = Orm::Tiny::Utils::Attribute; /*! JoinClause alias. */ using JoinClause = Orm::Query::JoinClause; @@ -439,14 +440,14 @@ namespace Tiny std::optional BuilderProxies::insert(const QVector &values) const { - return toBase().insert(TinyUtils::Attribute::convertVectorToMap(values)); + return toBase().insert(AttributeUtils::convertVectorToMap(values)); } template std::optional BuilderProxies::insert(const QVector> &values) const { - return toBase().insert(TinyUtils::Attribute::convertVectorsToMaps(values)); + return toBase().insert(AttributeUtils::convertVectorsToMaps(values)); } // FEATURE dilemma primarykey, Model::KeyType vs QVariant silverqx @@ -455,7 +456,7 @@ namespace Tiny BuilderProxies::insertGetId(const QVector &values, const QString &sequence) const { - return toBase().insertGetId(TinyUtils::Attribute::convertVectorToMap(values), + return toBase().insertGetId(AttributeUtils::convertVectorToMap(values), sequence); } @@ -463,8 +464,7 @@ namespace Tiny std::tuple> BuilderProxies::insertOrIgnore(const QVector &values) const { - return toBase().insertOrIgnore( - TinyUtils::Attribute::convertVectorToMap(values)); + return toBase().insertOrIgnore(AttributeUtils::convertVectorToMap(values)); } template @@ -472,8 +472,7 @@ namespace Tiny BuilderProxies::insertOrIgnore( const QVector> &values) const { - return toBase().insertOrIgnore( - TinyUtils::Attribute::convertVectorsToMaps(values)); + return toBase().insertOrIgnore(AttributeUtils::convertVectorsToMaps(values)); } template diff --git a/include/orm/tiny/types/syncchanges.hpp b/include/orm/tiny/types/syncchanges.hpp index a69a1837c..68deaa045 100644 --- a/include/orm/tiny/types/syncchanges.hpp +++ b/include/orm/tiny/types/syncchanges.hpp @@ -11,7 +11,7 @@ TINY_SYSTEM_HEADER #include #if defined(__clang__) || (defined(_MSC_VER) && _MSC_VER <= 1928) -#include +# include #endif #include "orm/macros/commonnamespace.hpp" diff --git a/include/orm/tiny/utils/attribute.hpp b/include/orm/tiny/utils/attribute.hpp index 0859736dd..6faa73e2e 100644 --- a/include/orm/tiny/utils/attribute.hpp +++ b/include/orm/tiny/utils/attribute.hpp @@ -9,33 +9,46 @@ TINY_SYSTEM_HEADER TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::Tiny::Utils::Attribute +namespace Orm::Tiny::Utils { - /*! Convert a AttributeItem QVector to QVariantMap. */ - SHAREDLIB_EXPORT QVariantMap - convertVectorToMap(const QVector &attributes); - /*! Convert a vector of AttributeItem QVectors to the vector of QVariantMaps. */ - SHAREDLIB_EXPORT QVector - convertVectorsToMaps(const QVector> &attributesVector); - /*! Convert a AttributeItem QVector to UpdateItem QVector. */ - SHAREDLIB_EXPORT QVector - convertVectorToUpdateItem(const QVector &attributes); - /*! Convert a AttributeItem QVector to UpdateItem QVector. */ - SHAREDLIB_EXPORT QVector - convertVectorToUpdateItem(QVector &&attributes); + /*! Library class for the database attribute. */ + class SHAREDLIB_EXPORT Attribute + { + Q_DISABLE_COPY(Attribute) - /*! Remove attributes which have duplicite keys and leave only the last one. */ - SHAREDLIB_EXPORT QVector - removeDuplicitKeys(const QVector &attributes); + public: + /*! Deleted default constructor, this is a pure library class. */ + Attribute() = delete; + /*! Deleted destructor. */ + ~Attribute() = delete; - /*! Join attributes and values for firstOrXx methods. */ - SHAREDLIB_EXPORT QVector - joinAttributesForFirstOr(const QVector &attributes, - const QVector &values, - const QString &keyName); + /*! Convert a AttributeItem QVector to QVariantMap. */ + static QVariantMap + convertVectorToMap(const QVector &attributes); + /*! Convert a vector of AttributeItem QVectors to the vector of QVariantMaps. */ + static QVector + convertVectorsToMaps(const QVector> &attributesVector); -} // namespace Orm::Tiny::Utils::Attribute + /*! Convert a AttributeItem QVector to UpdateItem QVector. */ + static QVector + convertVectorToUpdateItem(const QVector &attributes); + /*! Convert a AttributeItem QVector to UpdateItem QVector. */ + static QVector + convertVectorToUpdateItem(QVector &&attributes); + + /*! Remove attributes which have duplicite keys and leave only the last one. */ + static QVector + removeDuplicitKeys(const QVector &attributes); + + /*! Join attributes and values for firstOrXx methods. */ + static QVector + joinAttributesForFirstOr(const QVector &attributes, + const QVector &values, + const QString &keyName); + }; + +} // namespace Orm::Tiny::Utils TINYORM_END_COMMON_NAMESPACE diff --git a/include/orm/tiny/utils/string.hpp b/include/orm/tiny/utils/string.hpp index a559e424d..7dfb6906b 100644 --- a/include/orm/tiny/utils/string.hpp +++ b/include/orm/tiny/utils/string.hpp @@ -7,23 +7,50 @@ TINY_SYSTEM_HEADER #include +#ifndef TINYORM_DISABLE_TOM +# include +#endif + #include "orm/macros/commonnamespace.hpp" #include "orm/macros/export.hpp" TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::Tiny::Utils::String +namespace Orm::Tiny::Utils { - /*! Convert a string to snake case. */ - SHAREDLIB_EXPORT QString toSnake(QString string); - /*! Get the singular form of an English word. */ - SHAREDLIB_EXPORT QString singular(const QString &string); + /*! String related library class. */ + class SHAREDLIB_EXPORT String + { + Q_DISABLE_COPY(String) - /*! Check if the given string is the number, signed or unsigned. */ - SHAREDLIB_EXPORT bool isNumber(const QString &string); + public: + /*! Deleted default constructor, this is a pure library class. */ + String() = delete; + /*! Deleted destructor. */ + ~String() = delete; -} // namespace Orm::Tiny::Utils::String + /*! Convert a string to snake case. */ + static QString snake(QString string, QChar delimiter = '_'); + /*! Convert a value to studly caps case. */ + static QString studly(QString string); + + /*! Check if the given string is the number, signed or unsigned. */ + static bool isNumber(const QString &string, bool allowFloating = false); + +#ifndef TINYORM_DISABLE_TOM + /*! Split a string by the given width (not in the middle of a word). */ + static std::vector + splitStringByWidth(const QString &string, int width); +#endif + +#ifndef TINYORM_DISABLE_ORM + /*! Get the singular form of an English word. */ + static QString singular(const QString &string); +#endif + }; + +} // namespace Orm::Tiny::Utils TINYORM_END_COMMON_NAMESPACE diff --git a/include/orm/utils/type.hpp b/include/orm/utils/type.hpp index 3d482573b..703e07b2d 100644 --- a/include/orm/utils/type.hpp +++ b/include/orm/utils/type.hpp @@ -11,7 +11,7 @@ TINY_SYSTEM_HEADER #include #ifdef __GNUG__ -#include +# include #endif #include "orm/macros/commonnamespace.hpp" @@ -59,6 +59,9 @@ namespace Orm::Utils /*! Return a pretty function name in the following format: Xyz::function. */ static QString prettyFunction(const QString &function); + /*! Determine whether a string is true bool value (false for "", "0", "false"). */ + static bool isTrue(const QString &value); + private: /*! Class name with or w/o a namespace and w/o template parameters, common code. */ diff --git a/include/orm/version.hpp b/include/orm/version.hpp index 32a0cfdbb..1dc4b1409 100644 --- a/include/orm/version.hpp +++ b/include/orm/version.hpp @@ -4,7 +4,7 @@ // Excluded for the Resource compiler #ifndef RC_INVOKED -#include "orm/macros/systemheader.hpp" +# include "orm/macros/systemheader.hpp" TINY_SYSTEM_HEADER #endif diff --git a/include/pch.h b/include/pch.h index f843534af..c4e76dd5c 100644 --- a/include/pch.h +++ b/include/pch.h @@ -5,6 +5,7 @@ #if defined __cplusplus /* Add C++ includes here */ #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/qmake/TinyOrm.pri b/qmake/TinyOrm.pri index c36eb2aa4..886472630 100644 --- a/qmake/TinyOrm.pri +++ b/qmake/TinyOrm.pri @@ -30,3 +30,11 @@ CONFIG(release, debug|release): \ # Log queries with a time measurement in debug build CONFIG(release, debug|release): \ DEFINES *= TINYORM_NO_DEBUG_SQL + +# TinyTom related defines +# --- + +# Release build +CONFIG(release, debug|release): DEFINES += TINYTOM_NO_DEBUG +# Debug build +CONFIG(debug, debug|release): DEFINES *= TINYTOM_DEBUG diff --git a/qmake/common.pri b/qmake/common.pri index 2e7ebbbca..c6611d72c 100644 --- a/qmake/common.pri +++ b/qmake/common.pri @@ -30,11 +30,25 @@ DEFINES *= QT_NO_CAST_FROM_BYTEARRAY DEFINES *= QT_USE_QSTRINGBUILDER DEFINES *= QT_STRICT_ITERATORS +# TinyORM configuration +# --- + +# Use extern constants for shared build +CONFIG(shared, dll|shared|static|staticlib) | \ +CONFIG(dll, dll|shared|static|staticlib): \ + # Support override because inline_constants can be used in the shared build too + !inline_constants: \ + CONFIG += extern_constants + +# Archive library build (static build) +else: \ + CONFIG += inline_constants + # TinyORM defines # --- # Release build -CONFIG(release, debug|release): DEFINES *= TINYORM_NO_DEBUG +CONFIG(release, debug|release): DEFINES += TINYORM_NO_DEBUG # Debug build CONFIG(debug, debug|release): DEFINES *= TINYORM_DEBUG @@ -42,9 +56,27 @@ CONFIG(debug, debug|release): DEFINES *= TINYORM_DEBUG mysql_ping: DEFINES *= TINYORM_MYSQL_PING # Log queries with a time measurement -CONFIG(release, debug|release): DEFINES *= TINYORM_NO_DEBUG_SQL +CONFIG(release, debug|release): DEFINES += TINYORM_NO_DEBUG_SQL CONFIG(debug, debug|release): DEFINES *= TINYORM_DEBUG_SQL +# Enable code needed by tests, eg. connection overriding in the Model +!disable_orm:build_tests: \ + DEFINES *= TINYORM_TESTS_CODE + +# TinyTom related defines +# --- + +!disable_tom { + # Release build + CONFIG(release, debug|release): DEFINES += TINYTOM_NO_DEBUG + # Debug build + CONFIG(debug, debug|release): DEFINES *= TINYTOM_DEBUG + + # Enable code needed by tests (modify the migrate:status command for tests need) + build_tests: \ + DEFINES *= TINYTOM_TESTS_CODE +} + # Platform specific configuration # --- win32: include(winconf.pri) @@ -62,17 +94,3 @@ debug_and_release: { TINY_RELEASE_TYPE = $$quote(/debug) } else: TINY_RELEASE_TYPE = - -# Other -# --- - -# Use extern constants for shared build -CONFIG(shared, dll|shared|static|staticlib) | \ -CONFIG(dll, dll|shared|static|staticlib): \ - # Support override because inline_constants can be used in the shared build too - !inline_constants: \ - CONFIG += extern_constants - -# Archive library build (static build) -else: \ - CONFIG += inline_constants diff --git a/qmake/features/disable_tom.prf b/qmake/features/disable_tom.prf new file mode 100644 index 000000000..ce27c8c70 --- /dev/null +++ b/qmake/features/disable_tom.prf @@ -0,0 +1,2 @@ +CONFIG *= disable_tom +DEFINES *= TINYORM_DISABLE_TOM diff --git a/qmake/features/tiny_resource_and_manifest.prf b/qmake/features/tiny_resource_and_manifest.prf index 6d8177722..34c98c131 100644 --- a/qmake/features/tiny_resource_and_manifest.prf +++ b/qmake/features/tiny_resource_and_manifest.prf @@ -14,16 +14,12 @@ defineTest(tiny_resource_and_manifest) { isEmpty(2): resourcesFolder = $$absolute_path(resources, $$_PRO_FILE_PWD_) else: resourcesFolder = $$absolute_path($$2) - # Processing of RC file and manifest file for the test? - defined(3, var):$$3: isTest = true - else: isTest = false - # Target's extension contains(TEMPLATE, ".*app"): targetExt = ".exe" else:contains(TEMPLATE, ".*lib"): targetExt = ".dll" # Windows Resource file - rcFile = $$tiny_configure_cmake_rc($$resourcesFolder, $$targetExt, $$isTest) + rcFile = $$tiny_configure_cmake_rc($$resourcesFolder, $$targetExt, $$3, $$4) # Needed in the RC file, MinGW does not define _DEBUG macro mingw:CONFIG(debug, debug|release): DEFINES += _DEBUG @@ -34,8 +30,8 @@ defineTest(tiny_resource_and_manifest) { # Manifest file CONFIG -= embed_manifest_dll - # Use the same manifest file for all tests - $$isTest: manifestBasename = TinyTest + # Allow to pass RC/manifest file basename as 3. argument (default is a $$TARGET value) + defined(3, var):!isEmpty(3): manifestBasename = $$3 else: manifestBasename = $$TARGET # On MSVC use EMBED and on MinGW injected through the RC file @@ -56,7 +52,7 @@ defineTest(tiny_resource_and_manifest) { # RC file than manage two practically the same files. defineReplace(tiny_configure_cmake_rc) { # All tests use the same test.rc.in file - defined(3, var):$$3: rcBasename = TinyTest + defined(3, var):!isEmpty(3): rcBasename = $$3 else: rcBasename = $$TARGET rcFile = $$absolute_path($$1/$${rcBasename}.rc.in) @@ -75,8 +71,9 @@ defineReplace(tiny_configure_cmake_rc) { tiny_manifest_basename = $$rcBasename - # The same logic for the substitution token, is 'test' for tests and $$TARGET instead - token = $$rcBasename + # Allow to pass a custom token as 4. argument (default is a $$TARGET value) + defined(4, var):!isEmpty(4): token = $$4 + else: token = $$rcBasename rcFileContent = $$cat($$rcFile, blob) diff --git a/qmake/features/tiny_version_numbers.prf b/qmake/features/tiny_version_numbers.prf index 501a4c678..a99f21db9 100644 --- a/qmake/features/tiny_version_numbers.prf +++ b/qmake/features/tiny_version_numbers.prf @@ -1,11 +1,29 @@ # Find version numbers in the version header file and assign them to the # _VERSION_ and also to the VERSION variable. defineTest(tiny_version_numbers) { - versionHeader = $$find(HEADERS, "(?:.*\/)?version\.h(?:pp)?$") - exists(versionHeader)|isEmpty(versionHeader) { + # version.hpp for the TinyORM library project + contains(_PRO_FILE_, ".*\/src\/src\.pro$"): \ + versionHeader = $$absolute_path("../include/orm/version.hpp") + + # version.hpp for the tom example project + else:contains(_PRO_FILE_, ".*\/examples\/tom\/tom\.pro$"): \ + versionHeader = $$absolute_path("../tom/include/tom/version.hpp") + + else { + # Try to find in the HEADERS + versionHeader = $$find(HEADERS, "(?:.*\/)?version\.h(?:pp)?$") + + # Try to find on the INCLUDEPATH + isEmpty(versionHeader) { + versionHeaders = $$files("$$clean_path($$first(INCLUDEPATH))/version.hpp", \ + true) + versionHeader = $$first(versionHeaders) + } + } + + exists(versionHeader)|isEmpty(versionHeader): \ error( "HEADERS does not contain a version header file version.hpp, needed\ in the tiny_version_numbers.prf." ) - } versionFileContent = $$cat($$quote($$absolute_path($$versionHeader)), lines) versionTokens = MAJOR MINOR BUGFIX BUILD STATUS @@ -38,9 +56,8 @@ defineTest(tiny_version_numbers) { $$hasStatus: versionStatus = $$take_last(versionList) # Use 3 numbers version on other platforms - !win32 { + !win32: \ $$take_last(versionList) - } VERSION = $$join(versionList, '.')$$versionStatus export(VERSION) diff --git a/qmake/features/tom_example.prf b/qmake/features/tom_example.prf new file mode 100644 index 000000000..9977a12b2 --- /dev/null +++ b/qmake/features/tom_example.prf @@ -0,0 +1,2 @@ +CONFIG *= tom_example +DEFINES *= TINYORM_TOM_EXAMPLE diff --git a/qmake/tom.pri b/qmake/tom.pri new file mode 100644 index 000000000..412d612bd --- /dev/null +++ b/qmake/tom.pri @@ -0,0 +1,54 @@ +TINYORM_SOURCE_TREE = $$clean_path($$quote($$PWD/..)) +TINYTOM_SOURCE_TREE = $$quote($$TINYORM_SOURCE_TREE/tom) + +# Qt Common Configuration +# --- + +QT *= core sql + +CONFIG *= link_prl + +include($$TINYORM_SOURCE_TREE/qmake/common.pri) + +# Configure TinyORM library +# --- +# everything other is defined in the qmake/common.pri + +# Link with the shared library +CONFIG(shared, dll|shared|static|staticlib) | \ +CONFIG(dll, dll|shared|static|staticlib): \ + DEFINES *= TINYORM_LINKING_SHARED + +# File version +# --- + +# Find version numbers in the version header file and assign them to the +# _VERSION_ and also to the VERSION variable. +load(tiny_version_numbers) +tiny_version_numbers() + +# Windows resource and manifest files +# --- + +# Find version.hpp +tinyRcIncludepath = $$quote($$TINYTOM_SOURCE_TREE/include/) +# Find Windows manifest +mingw: tinyRcIncludepath += $$quote($$TINYTOM_SOURCE_TREE/resources/) + +load(tiny_resource_and_manifest) +tiny_resource_and_manifest( \ + $$tinyRcIncludepath, $$TINYTOM_SOURCE_TREE/resources, tom, TomExample \ +) + +# Link against TinyORM library +# --- + +INCLUDEPATH *= \ + $$quote($$TINYORM_SOURCE_TREE/include/) \ + $$quote($$TINYTOM_SOURCE_TREE/include/) \ + +!isEmpty(TINYORM_BUILD_TREE): \ +exists($$TINYORM_BUILD_TREE): { + LIBS += $$quote(-L$$TINYORM_BUILD_TREE/src$${TINY_RELEASE_TYPE}/) + LIBS += -lTinyOrm +} diff --git a/src/orm/concerns/logsqueries.cpp b/src/orm/concerns/logsqueries.cpp index 8d18f018f..29d742c29 100644 --- a/src/orm/concerns/logsqueries.cpp +++ b/src/orm/concerns/logsqueries.cpp @@ -1,13 +1,13 @@ #include "orm/concerns/logsqueries.hpp" #ifdef TINYORM_DEBUG_SQL -#include +# include #endif #include "orm/databaseconnection.hpp" #include "orm/macros/likely.hpp" #ifdef TINYORM_DEBUG_SQL -#include "orm/utils/query.hpp" +# include "orm/utils/query.hpp" #endif #ifdef TINYORM_DEBUG_SQL diff --git a/src/orm/databaseconnection.cpp b/src/orm/databaseconnection.cpp index 7dea5fec5..29724602d 100644 --- a/src/orm/databaseconnection.cpp +++ b/src/orm/databaseconnection.cpp @@ -2,7 +2,7 @@ #include #if defined(TINYORM_MYSQL_PING) -#include +# include #endif #include "orm/query/querybuilder.hpp" @@ -400,6 +400,14 @@ void DatabaseConnection::disconnect() m_qtConnectionResolver = nullptr; } +const SchemaGrammar &DatabaseConnection::getSchemaGrammar() +{ + if (!m_schemaGrammar) + useDefaultSchemaGrammar(); + + return *m_schemaGrammar; +} + std::unique_ptr DatabaseConnection::getSchemaBuilder() { if (!m_schemaGrammar) @@ -432,7 +440,7 @@ namespace { using DriverNameMapType = std::unordered_map; - // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + /*! Map Qt driver name to the pretty name. */ Q_GLOBAL_STATIC_WITH_ARGS(DriverNameMapType, DRIVER_NAME_MAP, ({ {QMYSQL, MYSQL_}, {QPSQL, POSTGRESQL}, diff --git a/src/orm/exceptions/logicerror.cpp b/src/orm/exceptions/logicerror.cpp index 5d33bfa6a..d69497cca 100644 --- a/src/orm/exceptions/logicerror.cpp +++ b/src/orm/exceptions/logicerror.cpp @@ -13,6 +13,10 @@ LogicError::LogicError(const QString &message) : std::logic_error(message.toUtf8().constData()) {} +LogicError::LogicError(const std::string &message) + : std::logic_error(message) +{} + } // namespace Orm::Exceptions TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/exceptions/runtimeerror.cpp b/src/orm/exceptions/runtimeerror.cpp index 97cb80394..1d470d42c 100644 --- a/src/orm/exceptions/runtimeerror.cpp +++ b/src/orm/exceptions/runtimeerror.cpp @@ -13,6 +13,10 @@ RuntimeError::RuntimeError(const QString &message) : std::runtime_error(message.toUtf8().constData()) {} +RuntimeError::RuntimeError(const std::string &message) + : std::runtime_error(message) +{} + } // namespace Orm::Exceptions TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/exceptions/sqlerror.cpp b/src/orm/exceptions/sqlerror.cpp index c1608426a..92676a1c4 100644 --- a/src/orm/exceptions/sqlerror.cpp +++ b/src/orm/exceptions/sqlerror.cpp @@ -26,11 +26,6 @@ SqlError::SqlError(const QString &message, const QSqlError &error, const int /*u , m_sqlError(error) {} -const QSqlError &SqlError::getSqlError() const -{ - return m_sqlError; -} - QString SqlError::formatMessage(const char *message, const QSqlError &error) const { QString result(message); diff --git a/src/orm/mysqlconnection.cpp b/src/orm/mysqlconnection.cpp index 34910c783..ef9cc0395 100644 --- a/src/orm/mysqlconnection.cpp +++ b/src/orm/mysqlconnection.cpp @@ -1,7 +1,7 @@ #include "orm/mysqlconnection.hpp" #ifdef TINYORM_MYSQL_PING -#include +# include #endif #include diff --git a/src/orm/tiny/utils/attribute.cpp b/src/orm/tiny/utils/attribute.cpp index b33cc2995..e564e3860 100644 --- a/src/orm/tiny/utils/attribute.cpp +++ b/src/orm/tiny/utils/attribute.cpp @@ -9,11 +9,10 @@ TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::Tiny::Utils::Attribute +namespace Orm::Tiny::Utils { -QVariantMap -convertVectorToMap(const QVector &attributes) +QVariantMap Attribute::convertVectorToMap(const QVector &attributes) { QVariantMap result; @@ -24,7 +23,7 @@ convertVectorToMap(const QVector &attributes) } QVector -convertVectorsToMaps(const QVector> &attributesVector) +Attribute::convertVectorsToMaps(const QVector> &attributesVector) { const auto size = attributesVector.size(); QVector result(size); @@ -37,7 +36,7 @@ convertVectorsToMaps(const QVector> &attributesVector) } QVector -convertVectorToUpdateItem(const QVector &attributes) +Attribute::convertVectorToUpdateItem(const QVector &attributes) { QVector result; result.reserve(attributes.size()); @@ -49,7 +48,7 @@ convertVectorToUpdateItem(const QVector &attributes) } QVector -convertVectorToUpdateItem(QVector &&attributes) +Attribute::convertVectorToUpdateItem(QVector &&attributes) { QVector result; result.reserve(attributes.size()); @@ -62,7 +61,7 @@ convertVectorToUpdateItem(QVector &&attributes) } QVector -removeDuplicitKeys(const QVector &attributes) +Attribute::removeDuplicitKeys(const QVector &attributes) { const auto size = attributes.size(); std::unordered_set added(static_cast(size)); @@ -87,9 +86,9 @@ removeDuplicitKeys(const QVector &attributes) } QVector -joinAttributesForFirstOr(const QVector &attributes, - const QVector &values, - const QString &keyName) +Attribute::joinAttributesForFirstOr(const QVector &attributes, + const QVector &values, + const QString &keyName) { // Remove the primary key from attributes auto attributesFiltered = @@ -126,6 +125,6 @@ joinAttributesForFirstOr(const QVector &attributes, return attributesFiltered + valuesFiltered; } -} // namespace Orm::Tiny::Utils::Attribute +} // namespace Orm::Tiny::Utils TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/tiny/utils/string.cpp b/src/orm/tiny/utils/string.cpp index 710f85a93..2ae54dc26 100644 --- a/src/orm/tiny/utils/string.cpp +++ b/src/orm/tiny/utils/string.cpp @@ -1,20 +1,47 @@ #include "orm/tiny/utils/string.hpp" -#include +#include + +#include #include "orm/constants.hpp" +using Orm::Constants::DASH; +using Orm::Constants::DOT; using Orm::Constants::MINUS; using Orm::Constants::PLUS; +using Orm::Constants::SPACE; using Orm::Constants::UNDERSCORE; TINYORM_BEGIN_COMMON_NAMESPACE -namespace Orm::Tiny::Utils::String +namespace Orm::Tiny::Utils { -QString toSnake(QString string) +/* This is only one translation unit from the Tiny namespace also used in the tom + project, so I leave it enabled in the build system when the tom is going to build, + I will not extract these 3 used methods to own dll or static library, they simply + will be built into the tinyorm shared library because of this + #ifndef TINYORM_DISABLE_ORM/TOM exists, methods are enabled/disabled on the base of + whether the orm or tom is built. */ + +/* public */ + +namespace { + using SnakeCache = std::unordered_map; + + /*! Snake cache for already computed strings. */ + Q_GLOBAL_STATIC(SnakeCache, snakeCache); // NOLINT(readability-redundant-member-init) +} // namespace + +QString String::snake(QString string, const QChar delimiter) +{ + auto key = string; + + if (snakeCache->contains(key)) + return (*snakeCache)[key]; + // RegExp not used for performance reasons std::vector positions; positions.reserve(static_cast(string.size() / 2) + 2); @@ -33,23 +60,60 @@ QString toSnake(QString string) } // Positions stay valid after inserts because reverse iterators used - std::for_each(positions.crbegin(), positions.crend(), [&string](const int pos) + std::for_each(positions.crbegin(), positions.crend(), + [&string, delimiter](const int pos) { - string.insert(pos, UNDERSCORE); + string.insert(pos, delimiter); }); - return string.toLower(); + return (*snakeCache)[std::move(key)] = string.toLower();; } -QString singular(const QString &string) +namespace { - if (!string.endsWith(QChar('s'))) + using StudlyCache = std::unordered_map; + + /*! Studly cache for already computed strings. */ + Q_GLOBAL_STATIC(StudlyCache, studlyCache); // NOLINT(readability-redundant-member-init) +} // namespace + +QString String::studly(QString string) +{ + auto value = string.trimmed(); + + // Nothing to do + if (value.isEmpty()) return string; - return string.chopped(1); + // Cache key + auto key = value; + + if (studlyCache->contains(key)) + return (*studlyCache)[key]; + + value.replace(DASH, SPACE) + .replace(UNDERSCORE, SPACE); + + auto size = value.size(); + + // Always upper a first character + if (size > 1) + value[0] = value[0].toUpper(); + + QString::size_type pos = 0; + + while ((pos = value.indexOf(SPACE, pos)) != -1) { + // Avoid out of bound exception + if (++pos >= size) + break; + + value[pos] = value[pos].toUpper(); + } + + return (*studlyCache)[std::move(key)] = value.replace(SPACE, ""); } -bool isNumber(const QString &string) +bool String::isNumber(const QString &string, const bool allowFloating) { /* Performance boost was amazing after the QRegularExpression has been removed, around 50% on the Playground app 👀, from 800ms to 400ms. */ @@ -60,15 +124,152 @@ bool isNumber(const QString &string) if (string.front() == PLUS || string.front() == MINUS) ++itBegin; + // Only one dot allowed + auto dotAlreadyFound = false; + const auto *nonDigit = std::find_if(itBegin, string.cend(), - [](const auto &ch) + [allowFloating, &dotAlreadyFound](const auto &ch) { - return !std::isdigit(ch.toLatin1()); + // Integer type + if (!allowFloating) + return !std::isdigit(ch.toLatin1()); + + // Floating-point type + // Only one dot allowed + const auto isDot = ch.toLatin1() == DOT; + + const auto result = !std::isdigit(ch.toLatin1()) && + (!isDot || (isDot && dotAlreadyFound)); + + if (isDot) + dotAlreadyFound = true; + + return result; }); return nonDigit == string.cend(); } -} // namespace Orm::Tiny::Utils::String +#ifndef TINYORM_DISABLE_TOM +namespace +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + using StringViewType = QStringView; +#else + using StringViewType = QStringRef; +#endif + + /*! Split the token to multiple lines by the given width. */ + bool splitLongToken(StringViewType token, const int width, QString &line, + std::vector &lines) + { + auto shouldContinue = false; + + const auto spaceSize = line.isEmpty() ? 0 : 1; + + if (const auto emptySpace = width - line.size() + spaceSize; + token.size() > emptySpace + ) { + // If on the line is still more than 30% of an empty space, use/fill it + if (emptySpace > std::llround(static_cast(width) * 0.3F)) { + // Position where to split the token + auto pos = width - line.size() - spaceSize; + + // Don't prepend the space at beginning of the line + if (!line.isEmpty()) + line.append(SPACE); + + line.append(token.left(pos)); + // Cut the appended part + token = token.mid(pos); + } + + // In every case no more space on the line here, push to lines + lines.emplace_back(std::move(line)); + // Start a new line + line.clear(); // NOLINT(bugprone-use-after-move) + + // Process a long token or rest of the token after the previous 30% filling + while (!token.isEmpty()) { + // Token is shorter than the width, indicates processing of the last token + if (token.size() <= width) { + line.append(token); // NOLINT(bugprone-use-after-move) + break; + } + + // Fill the whole line + line.append(token.left(width)); + // Cut the appended part + token = token.mid(width); + // Push to lines + lines.emplace_back(std::move(line)); + // Start a new line + line.clear(); // NOLINT(bugprone-use-after-move) + } + + shouldContinue = true; + } + + return shouldContinue; + } +} // namespace + +/*! Split a string by the given width (not in the middle of a word). */ +std::vector String::splitStringByWidth(const QString &string, const int width) +{ + // Nothing to split + if (string.size() <= width) + return {string}; + + std::vector lines; + QString line; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + for (auto token : string.tokenize(SPACE)) { +#else + for (auto token : string.splitRef(SPACE)) { // NOLINT(performance-for-range-copy) clazy:exclude=range-loop +#endif + /* If there is still a space on the line then append the token */ + if (line.size() + token.size() + 1 <= width) { + // Don't prepend the space at beginning of an empty line + if (!line.isEmpty()) + line.append(SPACE); + + line.append(token); + continue; + } + + /* If a token is longer than the width or an empty space on the current line */ + if (splitLongToken(token, width, line, lines)) + continue; + + // No space on the line, push to lines and start a new line + lines.emplace_back(std::move(line)); + + // Start a new line + line.clear(); // NOLINT(bugprone-use-after-move) + line.append(token); + } + + /* This can happen if a simple append of the token was the last operation, can happen + on the two places above. */ + if (!line.isEmpty()) + lines.emplace_back(std::move(line)); + + return lines; +} +#endif + +#ifndef TINYORM_DISABLE_ORM +QString String::singular(const QString &string) +{ + if (!string.endsWith(QChar('s'))) + return string; + + return string.chopped(1); +} +#endif + +} // namespace Orm::Tiny::Utils TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/utils/thread.cpp b/src/orm/utils/thread.cpp index 06c4c47db..670631e41 100644 --- a/src/orm/utils/thread.cpp +++ b/src/orm/utils/thread.cpp @@ -4,11 +4,11 @@ #if !defined(__clang__) && \ !defined(TINYORM_NO_DEBUG) && defined(_MSC_VER) && !defined(Q_OS_WINRT) -#include +# include #endif #if defined(Q_OS_LINUX) && !defined(QT_LINUXBASE) -#include +# include #endif TINYORM_BEGIN_COMMON_NAMESPACE diff --git a/src/orm/utils/type.cpp b/src/orm/utils/type.cpp index 3484c8667..b73fa5957 100644 --- a/src/orm/utils/type.cpp +++ b/src/orm/utils/type.cpp @@ -3,12 +3,12 @@ #include #if !defined(_MSC_VER) -#include +# include #endif #include "orm/constants.hpp" #if !defined(_MSC_VER) -#include "orm/exceptions/runtimeerror.hpp" +# include "orm/exceptions/runtimeerror.hpp" #endif using Orm::Constants::ASTERISK_C; @@ -57,6 +57,11 @@ QString Type::prettyFunction(const QString &function) return QStringLiteral("%1::%2").arg(match.captured(1), match.captured(2)); } +bool Type::isTrue(const QString &value) +{ + return !value.isEmpty() && value != "0" && value != "false"; +} + QString Type::classPureBasenameInternal(const std::type_info &typeInfo, const bool withNamespace) { diff --git a/src/src.pri b/src/src.pri index d67abe304..db20f5f81 100644 --- a/src/src.pri +++ b/src/src.pri @@ -61,6 +61,9 @@ sourcesList += \ $$PWD/orm/tiny/tinytypes.cpp \ $$PWD/orm/tiny/types/syncchanges.cpp \ $$PWD/orm/tiny/utils/attribute.cpp \ + +!disable_orm|!disable_tom: \ + sourcesList += \ $$PWD/orm/tiny/utils/string.cpp \ SOURCES += $$sorted(sourcesList) diff --git a/src/src.pro b/src/src.pro index 1ab7a9bcc..5303f3b3a 100644 --- a/src/src.pro +++ b/src/src.pro @@ -39,10 +39,6 @@ CONFIG(shared, dll|shared|static|staticlib) | \ CONFIG(dll, dll|shared|static|staticlib): \ DEFINES *= TINYORM_BUILDING_SHARED -# Enable code needed by tests, eg. connection overriding in the Model -build_tests: \ - DEFINES *= TINYORM_TESTS_CODE - # TinyORM library header and source files # --- @@ -50,6 +46,14 @@ build_tests: \ include(../include/include.pri) include(src.pri) +# TinyTom header and source files +# --- + +!disable_tom { + include(../tom/include/include.pri) + include(../tom/src/src.pri) +} + # File version # --- @@ -85,8 +89,16 @@ win32-msvc:CONFIG(debug, debug|release) { # Some info output # --- -CONFIG(debug, debug|release):!build_pass: message( "Project is built in DEBUG mode." ) -CONFIG(release, debug|release):!build_pass: message( "Project is built in RELEASE mode." ) +!build_pass { + CONFIG(debug, debug|release): message( "Project is built in DEBUG mode." ) + CONFIG(release, debug|release): message( "Project is built in RELEASE mode." ) + + !disable_orm: message("Build ORM-related source code.") + else: message("Disable ORM-related source code (build the query builder \ +only).") + + mysql_ping: message("Enable MySQL ping on Orm::MySqlConnection.") +} # User Configuration # --- diff --git a/tests/TinyUtils/src/databases.cpp b/tests/TinyUtils/src/databases.cpp index f0aad9c40..02bffe8be 100644 --- a/tests/TinyUtils/src/databases.cpp +++ b/tests/TinyUtils/src/databases.cpp @@ -39,6 +39,10 @@ using Orm::DB; using Orm::Exceptions::RuntimeError; +#ifndef TINYORM_SQLITE_DATABASE +# define TINYORM_SQLITE_DATABASE "" +#endif + namespace TestUtils { @@ -49,6 +53,14 @@ namespace TestUtils correctly. Tests don't fail but are skipped when a connection is not available. */ +namespace +{ + /*! DatabaseManager instance. */ + Q_GLOBAL_STATIC_WITH_ARGS(std::shared_ptr, db, {nullptr}); +} + +/* public */ + const QStringList &Databases::createConnections(const QStringList &connections) { throwIfConnectionsInitialized(); @@ -57,7 +69,7 @@ const QStringList &Databases::createConnections(const QStringList &connections) /* The default connection is empty for tests, there is no default connection because it can produce hard to find bugs, I have to be explicit about the connection which will be used. */ - static const auto manager = DB::create(getConfigurations(connections), ""); + static const auto manager = *db = DB::create(getConfigurations(connections), ""); static const auto cachedConnectionNames = manager->connectionNames(); @@ -88,6 +100,16 @@ bool Databases::allEnvVariablesEmpty(const std::vector &envVariabl }); } +const std::shared_ptr &Databases::manager() +{ + if (db() == nullptr) + throw RuntimeError("The global static 'db' was already destroyed."); + + return *db; +} + +/* private */ + const Databases::ConfigurationsType & Databases::getConfigurations(const QStringList &connections) { diff --git a/tests/TinyUtils/src/databases.hpp b/tests/TinyUtils/src/databases.hpp index 2485151e2..3970f9515 100644 --- a/tests/TinyUtils/src/databases.hpp +++ b/tests/TinyUtils/src/databases.hpp @@ -6,6 +6,11 @@ #include "export.hpp" +namespace Orm +{ + class DatabaseManager; +} + namespace TestUtils { @@ -34,6 +39,9 @@ namespace TestUtils /*! Check whether all env. variables are empty. */ static bool allEnvVariablesEmpty(const std::vector &envVariables); + /*! Get a reference to the database manager. */ + static const std::shared_ptr &manager(); + private: /*! Obtain configurations for the given connection names. */ static const ConfigurationsType & diff --git a/tests/TinyUtils/src/pch.h b/tests/TinyUtils/src/pch.h index 66b2580fd..e3204125b 100644 --- a/tests/TinyUtils/src/pch.h +++ b/tests/TinyUtils/src/pch.h @@ -6,6 +6,7 @@ /* Add C++ includes here */ //#include #include +//#include #include #include #include @@ -38,7 +39,8 @@ //#include #include //#include -#include +//#include +//#include //#include //#include #endif diff --git a/tests/auto/functional/CMakeLists.txt b/tests/auto/functional/CMakeLists.txt index 98453ac29..23f90d131 100644 --- a/tests/auto/functional/CMakeLists.txt +++ b/tests/auto/functional/CMakeLists.txt @@ -1 +1,5 @@ add_subdirectory(orm) + +if(TOM) + add_subdirectory(tom) +endif() diff --git a/tests/auto/functional/functional.pro b/tests/auto/functional/functional.pro index af3854e66..4ccecbad5 100644 --- a/tests/auto/functional/functional.pro +++ b/tests/auto/functional/functional.pro @@ -1,4 +1,12 @@ TEMPLATE = subdirs -SUBDIRS = \ +subdirsList = \ orm \ + +!disable_tom: \ + subdirsList += \ + tom \ + +SUBDIRS = $$sorted(subdirsList) + +unset(subdirsList) diff --git a/tests/auto/functional/tom/CMakeLists.txt b/tests/auto/functional/tom/CMakeLists.txt new file mode 100644 index 000000000..4c9d8c26f --- /dev/null +++ b/tests/auto/functional/tom/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(migrate) diff --git a/tests/auto/functional/tom/migrate/CMakeLists.txt b/tests/auto/functional/tom/migrate/CMakeLists.txt new file mode 100644 index 000000000..1a7a1551a --- /dev/null +++ b/tests/auto/functional/tom/migrate/CMakeLists.txt @@ -0,0 +1,18 @@ +# migrate auto test +# --- + +set(migrate_ns migrate) +set(migrate_target ${migrate_ns}) + +project(${migrate_ns} + LANGUAGES CXX +) + +add_executable(${migrate_target} + tst_migrate.cpp +) + +add_test(NAME ${migrate_target} COMMAND ${migrate_target}) + +include(TinyTestCommon) +tiny_configure_test(${migrate_target} INCLUDE_MIGRATIONS) diff --git a/tests/auto/functional/tom/migrate/migrate.pro b/tests/auto/functional/tom/migrate/migrate.pro new file mode 100644 index 000000000..921ca5c65 --- /dev/null +++ b/tests/auto/functional/tom/migrate/migrate.pro @@ -0,0 +1,5 @@ +include($$TINYORM_SOURCE_TREE/tests/qmake/common.pri) +include($$TINYORM_SOURCE_TREE/tests/qmake/TinyUtils.pri) +include($$TINYORM_SOURCE_TREE/tests/database/migrations.pri) + +SOURCES += tst_migrate.cpp diff --git a/tests/auto/functional/tom/migrate/tst_migrate.cpp b/tests/auto/functional/tom/migrate/tst_migrate.cpp new file mode 100644 index 000000000..7b5420338 --- /dev/null +++ b/tests/auto/functional/tom/migrate/tst_migrate.cpp @@ -0,0 +1,585 @@ +#include +#include + +#include "tom/application.hpp" +#include "tom/commands/migrations/statuscommand.hpp" + +#include "databases.hpp" + +#include "migrations/2014_10_12_000000_create_posts_table.hpp" +#include "migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp" +#include "migrations/2014_10_12_200000_create_properties_table.hpp" +#include "migrations/2014_10_12_300000_create_phones_table.hpp" + +using namespace Migrations; // NOLINT(google-build-using-namespace) + +using TomApplication = Tom::Application; + +using Tom::Commands::Migrations::StatusCommand; + +using TestUtils::Databases; + +class tst_Migrate : public QObject +{ + Q_OBJECT + +public: + /*! Alias for the test output row. */ + using StatusRow = StatusCommand::StatusRow; + /*! Type used for comparing results of the status command. */ + using Status = std::vector; + +private slots: + void initTestCase(); + void cleanup() const; + + void reset() const; + + void migrate() const; + void migrate_Step() const; + + void rollback_OnMigrate() const; + void rollback_OnMigrateWithStep() const; + + void rollback_Step_OnMigrate() const; + void rollback_Step_OnMigrateWithStep() const; + + void refresh_OnMigrate() const; + void refresh_OnMigrateWithStep() const; + + void refresh_Step() const; + void refresh_StepMigrate() const; + void refresh_Step_StepMigrate() const; + +// NOLINTNEXTLINE(readability-redundant-access-specifiers) +private: + /*! Prepare arguments and invoke runCommand(). */ + [[nodiscard]] int + invokeCommand(const QString &name, std::vector &&arguments = {}) const; + /*! Create a tom application instance and invoke the given command. */ + int runCommand(int &argc, const std::vector &argv) const; + + /*! Invoke the status command to obtain results. */ + inline int invokeTestStatusCommand() const; + /*! Get result of the last status command. */ + Status status() const; + /*! Create a status object for comparing with the result of the status(). */ + Status createStatus(std::initializer_list rows) const; + /*! Create a status object to be equal after complete rollback. */ + Status createResetStatus() const; + + /*! Connection name used in this test case. */ + QString m_connection {}; +}; + +/*! Alias for the test output row. */ +using Status = tst_Migrate::Status; +/*! Type used for comparing results of the status command. */ +using StatusRow = tst_Migrate::StatusRow; + +/* Extracted common code to re-use. */ +namespace +{ + // Status + inline const auto *Yes = "Yes"; + inline const auto *No = "No"; + + // Batches + inline const auto *s_1 = "1"; + inline const auto *s_2 = "2"; + inline const auto *s_3 = "3"; + inline const auto *s_4 = "4"; + + // Migration names + inline const auto * + s_2014_10_12_000000_create_posts_table = + "2014_10_12_000000_create_posts_table"; + inline const auto * + s_2014_10_12_100000_add_factor_column_to_posts_table = + "2014_10_12_100000_add_factor_column_to_posts_table"; + inline const auto * + s_2014_10_12_200000_create_properties_table = + "2014_10_12_200000_create_properties_table"; + inline const auto * + s_2014_10_12_300000_create_phones_table = + "2014_10_12_300000_create_phones_table"; + + // Fully migrated w/o --step + inline const std::initializer_list + FullyMigrated = { + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_1}, + {Yes, s_2014_10_12_200000_create_properties_table, s_1}, + {Yes, s_2014_10_12_300000_create_phones_table, s_1}, + }; + + // Fully migrated with --step + inline const std::initializer_list + FullyStepMigrated = { + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_2}, + {Yes, s_2014_10_12_200000_create_properties_table, s_3}, + {Yes, s_2014_10_12_300000_create_phones_table, s_4}, + }; + +} // namespace + +/* private slots */ + +void tst_Migrate::initTestCase() +{ + m_connection = Databases::createConnection(Databases::MYSQL); + + if (m_connection.isEmpty()) + QSKIP(QStringLiteral("%1 autotest skipped, environment variables " + "for '%2' connection have not been defined.") + .arg("tst_Migrate", Databases::MYSQL).toUtf8().constData(), ); + + /* Modify the migrate:status command to not output a status table to the console but + instead return a result as the vector, this vector is then used for comparing + results. */ + TomApplication::enableInUnitTests(); +} + +void tst_Migrate::cleanup() const +{ + /* All test methods need this except for two of them (reset and I don't remember + second), I will not implement special logic to skip this for these two methods. */ + { + auto exitCode = invokeCommand("migrate:reset"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createResetStatus(), status()); + } +} + +void tst_Migrate::reset() const +{ + { + auto exitCode = invokeCommand("migrate:reset"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createResetStatus(), status()); + } +} + +void tst_Migrate::migrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } +} + +void tst_Migrate::migrate_Step() const +{ + { + auto exitCode = invokeCommand("migrate", {"--step"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyStepMigrated), status()); + } +} + +void tst_Migrate::rollback_OnMigrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // rollback on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:rollback"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createResetStatus(), status()); + } +} + +void tst_Migrate::rollback_OnMigrateWithStep() const +{ + { + auto exitCode = invokeCommand("migrate", {"--step"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyStepMigrated), status()); + } + + // rollback on previous migrate with --step + { + auto exitCode = invokeCommand("migrate:rollback"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus({ + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_2}, + {Yes, s_2014_10_12_200000_create_properties_table, s_3}, + {No, s_2014_10_12_300000_create_phones_table}, + }), status()); + } +} + +void tst_Migrate::rollback_Step_OnMigrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // rollback on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:rollback", {"--step=2"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus({ + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_1}, + {No, s_2014_10_12_200000_create_properties_table}, + {No, s_2014_10_12_300000_create_phones_table}, + }), status()); + } +} + +void tst_Migrate::rollback_Step_OnMigrateWithStep() const +{ + { + auto exitCode = invokeCommand("migrate", {"--step"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyStepMigrated), status()); + } + + // rollback on previous migrate with --step + { + auto exitCode = invokeCommand("migrate:rollback", {"--step=2"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus({ + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_2}, + {No, s_2014_10_12_200000_create_properties_table}, + {No, s_2014_10_12_300000_create_phones_table}, + }), status()); + } +} + +void tst_Migrate::refresh_OnMigrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // refresh on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:refresh"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } +} + +void tst_Migrate::refresh_OnMigrateWithStep() const +{ + { + auto exitCode = invokeCommand("migrate", {"--step"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyStepMigrated), status()); + } + + // refresh on previous migrate with --step + { + auto exitCode = invokeCommand("migrate:refresh"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } +} + +void tst_Migrate::refresh_Step() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // refresh on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:refresh", {"--step=2"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus({ + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_1}, + {Yes, s_2014_10_12_200000_create_properties_table, s_2}, + {Yes, s_2014_10_12_300000_create_phones_table, s_2}, + }), status()); + } +} + +void tst_Migrate::refresh_StepMigrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // refresh on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:refresh", {"--step-migrate"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyStepMigrated), status()); + } +} + +void tst_Migrate::refresh_Step_StepMigrate() const +{ + { + auto exitCode = invokeCommand("migrate"); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus(FullyMigrated), status()); + } + + // refresh on previous migrate w/o --step + { + auto exitCode = invokeCommand("migrate:refresh", {"--step=2", "--step-migrate"}); + + QVERIFY(exitCode == EXIT_SUCCESS); + } + + { + auto exitCode = invokeTestStatusCommand(); + + QVERIFY(exitCode == EXIT_SUCCESS); + QCOMPARE(createStatus({ + {Yes, s_2014_10_12_000000_create_posts_table, s_1}, + {Yes, s_2014_10_12_100000_add_factor_column_to_posts_table, s_1}, + {Yes, s_2014_10_12_200000_create_properties_table, s_2}, + {Yes, s_2014_10_12_300000_create_phones_table, s_3}, + }), status()); + } +} + +/* private */ + +int tst_Migrate::invokeCommand(const QString &name, + std::vector &&arguments) const +{ + static const auto connectionTmpl = QStringLiteral("--database=%1"); + + // Prepare fake argc and argv + const auto nameArr = name.toUtf8(); + // FUTURE tests tom, when the schema builder will support more db drivers, I can run it on all supported connections, code will look like in the tst_querybuilder.cpp, then I will fetch connection name in every test method using QFETCH_GLOBAL() and I will pass this connection name to the invokeCommand(), so I will discard m_connection and will use method parameter connection here silverqx + /* Schema builder is implemented only for the MySQL driver, so I can use m_connection + here as the default connection. */ + // DB connection to use + const auto connectionArr = connectionTmpl.arg(m_connection).toUtf8(); + + std::vector argv { +#ifdef _WIN32 + "tom.exe", +#else + "tom", +#endif + nameArr.constData(), + connectionArr.constData(), + }; + std::ranges::move(arguments, std::back_inserter(argv)); + + int argc = static_cast(argv.size()); + + return runCommand(argc, argv); +} + +int tst_Migrate::runCommand(int &argc, const std::vector &argv) const +{ + try { + // env. should be always development so passed {} for env. name + return TomApplication(argc, const_cast(argv.data()), + Databases::manager(), {}) + .migrations< + _2014_10_12_000000_create_posts_table, + _2014_10_12_100000_add_factor_column_to_posts_table, + _2014_10_12_200000_create_properties_table, + _2014_10_12_300000_create_phones_table>() + // Fire it up 🔥🚀✨ + .runWithArguments({argv.cbegin(), argv.cend()}); + + } catch (const std::exception &e) { + + TomApplication::logException(e, true); + } + + return EXIT_FAILURE; +} + +int tst_Migrate::invokeTestStatusCommand() const +{ + return invokeCommand("migrate:status"); +} + +Status tst_Migrate::status() const +{ + return TomApplication::status(); +} + +Status tst_Migrate::createStatus(std::initializer_list rows) const +{ + return Status({rows}); +} + +Status tst_Migrate::createResetStatus() const +{ + return Status({ + {No, s_2014_10_12_000000_create_posts_table}, + {No, s_2014_10_12_100000_add_factor_column_to_posts_table}, + {No, s_2014_10_12_200000_create_properties_table}, + {No, s_2014_10_12_300000_create_phones_table}, + }); +} + +QTEST_MAIN(tst_Migrate) + +#include "tst_migrate.moc" diff --git a/tests/auto/functional/tom/tom.pro b/tests/auto/functional/tom/tom.pro new file mode 100644 index 000000000..6b273c9dd --- /dev/null +++ b/tests/auto/functional/tom/tom.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + migrate \ diff --git a/tests/auto/unit/orm/version/CMakeLists.txt b/tests/auto/unit/orm/version/CMakeLists.txt index ec7cc337f..f717854a6 100644 --- a/tests/auto/unit/orm/version/CMakeLists.txt +++ b/tests/auto/unit/orm/version/CMakeLists.txt @@ -21,6 +21,15 @@ if(BUILD_SHARED_LIBS) target_compile_definitions(version PRIVATE TINYTEST_VERSION_IS_SHARED_BUILD) endif() +if(TOM_EXAMPLE) + target_compile_definitions(version PRIVATE TINYTOM_EXAMPLE) + + # To find tom/include/version.hpp + target_include_directories(version PRIVATE + "$" + ) +endif() + target_include_directories(version PRIVATE "$" ) diff --git a/tests/auto/unit/orm/version/include/versiondebug_cmake.hpp.in b/tests/auto/unit/orm/version/include/versiondebug_cmake.hpp.in index 123b8ee01..8a9beb960 100644 --- a/tests/auto/unit/orm/version/include/versiondebug_cmake.hpp.in +++ b/tests/auto/unit/orm/version/include/versiondebug_cmake.hpp.in @@ -4,5 +4,6 @@ #define TINYTEST_VERSION_TINYORM_PATH "$" #define TINYTEST_VERSION_TINYUTILS_PATH "$" +#define TINYTEST_VERSION_TOMEXAMPLE_PATH "$<$:$>" #endif // TINYTESTS_VERSIONDEBUG_CMAKE_HPP diff --git a/tests/auto/unit/orm/version/include/versiondebug_qmake.hpp.in b/tests/auto/unit/orm/version/include/versiondebug_qmake.hpp.in index f1d3ec284..d083eab64 100644 --- a/tests/auto/unit/orm/version/include/versiondebug_qmake.hpp.in +++ b/tests/auto/unit/orm/version/include/versiondebug_qmake.hpp.in @@ -4,5 +4,6 @@ #define TINYTEST_VERSION_TINYORM_PATH \"$${TINYTEST_VERSION_TINYORM_PATH}\" #define TINYTEST_VERSION_TINYUTILS_PATH \"$${TINYTEST_VERSION_TINYUTILS_PATH}\" +#define TINYTEST_VERSION_TOMEXAMPLE_PATH \"$${TINYTEST_VERSION_TOMEXAMPLE_PATH}\" #endif // TINYTEST_VERSIONDEBUG_QMAKE_HPP diff --git a/tests/auto/unit/orm/version/tst_version.cpp b/tests/auto/unit/orm/version/tst_version.cpp index e5524fa74..e110d783b 100644 --- a/tests/auto/unit/orm/version/tst_version.cpp +++ b/tests/auto/unit/orm/version/tst_version.cpp @@ -2,7 +2,7 @@ #include #if defined(_WIN32) && defined(TINYTEST_VERSION_IS_SHARED_BUILD) -#include +# include #endif #include "fs.hpp" @@ -11,6 +11,10 @@ #include "orm/version.hpp" // TinyUtils #include "version.hpp" +// Tom example +#ifdef TINYTOM_EXAMPLE +# include "tom/version.hpp" +#endif // Used by checkFileVersion_*() tests #if defined(_WIN32) && defined(TINYTEST_VERSION_IS_SHARED_BUILD) @@ -22,6 +26,7 @@ #else # define TINYTEST_VERSION_TINYORM_PATH # define TINYTEST_VERSION_TINYUTILS_PATH +# define TINYTEST_VERSION_TOMEXAMPLE_PATH #endif #if defined(_WIN32) @@ -35,9 +40,15 @@ class tst_Version : public QObject private slots: void versions_TinyOrm() const; void versions_TinyUtils() const; +#ifdef TINYTOM_EXAMPLE + void versions_TomExample() const; +#endif void checkFileVersion_TinyOrm() const; void checkFileVersion_TinyUtils() const; +#ifdef TINYTOM_EXAMPLE + void checkFileVersion_TomExample() const; +#endif #if defined(_WIN32) && defined(TINYTEST_VERSION_IS_SHARED_BUILD) // NOLINTNEXTLINE(readability-redundant-access-specifiers) @@ -125,6 +136,43 @@ void tst_Version::versions_TinyUtils() const QCOMPARE(TINYUTILS_VERSION, version); } +#ifdef TINYTOM_EXAMPLE +void tst_Version::versions_TomExample() const +{ + // Test types + QCOMPARE(typeid (TINYTOM_VERSION_MAJOR), typeid (int)); + QCOMPARE(typeid (TINYTOM_VERSION_MINOR), typeid (int)); + QCOMPARE(typeid (TINYTOM_VERSION_BUGFIX), typeid (int)); + QCOMPARE(typeid (TINYTOM_VERSION_BUILD), typeid (int)); + + // Individual version numbers have to be greater than zero + QVERIFY(TINYTOM_VERSION_MAJOR >= 0); + QVERIFY(TINYTOM_VERSION_MINOR >= 0); + QVERIFY(TINYTOM_VERSION_BUGFIX >= 0); + QVERIFY(TINYTOM_VERSION_BUILD >= 0); + + // Project and File Version strings + QString versionStr = QString::number(TINYTOM_VERSION_MAJOR) + QChar('.') + + QString::number(TINYTOM_VERSION_MINOR) + QChar('.') + + QString::number(TINYTOM_VERSION_BUGFIX); + QString fileVersionStr = versionStr + QChar('.') + + QString::number(TINYTOM_VERSION_BUILD); + if constexpr (TINYTOM_VERSION_BUILD > 0) + versionStr += QChar('.') + QString::number(TINYTOM_VERSION_BUILD); + versionStr += TINYTOM_VERSION_STATUS; + + QCOMPARE(TINYTOM_FILEVERSION_STR, fileVersionStr); + QCOMPARE(TINYTOM_VERSION_STR, versionStr); + QCOMPARE(TINYTOM_VERSION_STR_2, QChar('v') + versionStr); + + // Project Version number, to check API compatibility + const auto version = TINYTOM_VERSION_MAJOR * 10000 + + TINYTOM_VERSION_MINOR * 100 + + TINYTOM_VERSION_BUGFIX; + QCOMPARE(TINYTOM_VERSION, version); +} +#endif + void tst_Version::checkFileVersion_TinyOrm() const { #if !defined(_WIN32) @@ -167,6 +215,29 @@ void tst_Version::checkFileVersion_TinyUtils() const #endif } +#ifdef TINYTOM_EXAMPLE +void tst_Version::checkFileVersion_TomExample() const +{ +#if !defined(_WIN32) + QSKIP("checkFileVersion_*() related tests are supported on MSVC only.", ); +#elif !defined(TINYTEST_VERSION_IS_SHARED_BUILD) + QSKIP("checkFileVersion_*() related tests are enabled for shared builds only.", ); +#else + const auto fileVersions = + getExeVersionString(Fs::absolutePath(TINYTEST_VERSION_TOMEXAMPLE_PATH)); + + // Project and File Version strings + const QString versionStr = QString::number(TINYTOM_VERSION_MAJOR) + QChar('.') + + QString::number(TINYTOM_VERSION_MINOR) + QChar('.') + + QString::number(TINYTOM_VERSION_BUGFIX) + QChar('.') + + QString::number(TINYTOM_VERSION_BUILD); + + QCOMPARE(fileVersions.productVersion, versionStr); + QCOMPARE(fileVersions.fileVersion, fileVersions.productVersion); +#endif +} +#endif + #if defined(_WIN32) && defined(TINYTEST_VERSION_IS_SHARED_BUILD) tst_Version::FileVersions tst_Version::getExeVersionString(const QString &fileName) const diff --git a/tests/auto/unit/orm/version/version.pro b/tests/auto/unit/orm/version/version.pro index 92c67760f..d86337583 100644 --- a/tests/auto/unit/orm/version/version.pro +++ b/tests/auto/unit/orm/version/version.pro @@ -7,6 +7,9 @@ SOURCES = tst_version.cpp win32 { DEFINES += TINYTEST_VERSION_IS_QMAKE + tom_example:!disable_tom: \ + DEFINES += TINYTOM_EXAMPLE + CONFIG(shared, dll|shared|static|staticlib) | \ CONFIG(dll, dll|shared|static|staticlib): \ DEFINES += TINYTEST_VERSION_IS_SHARED_BUILD @@ -15,6 +18,8 @@ win32 { $$quote($${TINYORM_BUILD_TREE}/src$${TINY_RELEASE_TYPE}/TinyOrm0.dll) TINYTEST_VERSION_TINYUTILS_PATH = \ $$quote($${TINYORM_BUILD_TREE}/tests/TinyUtils$${TINY_RELEASE_TYPE}/TinyUtils0.dll) + TINYTEST_VERSION_TOMEXAMPLE_PATH = \ + $$quote($${TINYORM_BUILD_TREE}/examples/tom$${TINY_RELEASE_TYPE}/tom.exe) QMAKE_SUBSTITUTES += $$quote(include/versiondebug_qmake.hpp.in) @@ -22,5 +27,9 @@ win32 { INCLUDEPATH += $$quote($$OUT_PWD/include/) + # To find tom/include/version.hpp (don't need to include whole qmake/tom.pri) + tom_example:!disable_tom: \ + INCLUDEPATH += $$quote($$TINYORM_SOURCE_TREE/tom/include/) + LIBS += -lVersion } diff --git a/tests/conf.pri.example b/tests/conf.pri.example index 73e4d3a76..adea66efc 100644 --- a/tests/conf.pri.example +++ b/tests/conf.pri.example @@ -6,19 +6,24 @@ win32-g++|win32-clang-g++ { # Enable ccache wrapper CONFIG *= tiny_ccache + # Includes + # tabulate + INCLUDEPATH += $$quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + QMAKE_CXXFLAGS += -isystem $$shell_quote(C:/msys64/home/xyz/vcpkg/installed/x64-mingw-dynamic/include/) + # Use faster linker # CONFIG *= use_lld_linker does not work on MinGW QMAKE_LFLAGS *= -fuse-ld=lld } else:win32-msvc { # Includes - # range-v3 + # range-v3 and tabulate INCLUDEPATH += $$quote(E:/xyz/vcpkg/installed/x64-windows/include/) } else:unix { # Includes - # range-v3 - QMAKE_CXXFLAGS += -isystem $$quote(/home/xyz/vcpkg/installed/x64-linux/include/) + # range-v3 and tabulate + QMAKE_CXXFLAGS += -isystem $$shell_quote(/home/xyz/vcpkg/installed/x64-linux/include/) # Use faster linkers clang: CONFIG *= use_lld_linker diff --git a/tests/database/migrations.pri b/tests/database/migrations.pri new file mode 100644 index 000000000..9a993ba7a --- /dev/null +++ b/tests/database/migrations.pri @@ -0,0 +1,7 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/migrations/2014_10_12_000000_create_posts_table.hpp \ + $$PWD/migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp \ + $$PWD/migrations/2014_10_12_200000_create_properties_table.hpp \ + $$PWD/migrations/2014_10_12_300000_create_phones_table.hpp \ diff --git a/tests/database/migrations/2014_10_12_000000_create_posts_table.hpp b/tests/database/migrations/2014_10_12_000000_create_posts_table.hpp new file mode 100644 index 000000000..45cf56136 --- /dev/null +++ b/tests/database/migrations/2014_10_12_000000_create_posts_table.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace Migrations +{ + + struct _2014_10_12_000000_create_posts_table : Migration + { + /*! Run the migrations. */ + void up() const override + { + Schema::create("posts", [](Blueprint &table) + { + table.id(); + + table.string(NAME); + table.timestamps(); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::dropIfExists("posts"); + } + }; + +} // namespace Migrations diff --git a/tests/database/migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp b/tests/database/migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp new file mode 100644 index 000000000..3f1a39f9f --- /dev/null +++ b/tests/database/migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace Migrations +{ + + struct _2014_10_12_100000_add_factor_column_to_posts_table : Migration + { + /*! Run the migrations. */ + void up() const override + { + Schema::table("posts", [](Blueprint &table) + { + table.integer("factor"); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::table("posts", [](Blueprint &table) + { + table.dropColumn("factor"); + }); + } + }; + +} // namespace Migrations diff --git a/tests/database/migrations/2014_10_12_200000_create_properties_table.hpp b/tests/database/migrations/2014_10_12_200000_create_properties_table.hpp new file mode 100644 index 000000000..995196b38 --- /dev/null +++ b/tests/database/migrations/2014_10_12_200000_create_properties_table.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace Migrations +{ + + struct _2014_10_12_200000_create_properties_table : Migration + { + /*! Run the migrations. */ + void up() const override + { + Schema::create("properties", [](Blueprint &table) + { + table.id(); + + table.string(NAME); + table.timestamps(); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::dropIfExists("properties"); + } + }; + +} // namespace Migrations diff --git a/tests/database/migrations/2014_10_12_300000_create_phones_table.hpp b/tests/database/migrations/2014_10_12_300000_create_phones_table.hpp new file mode 100644 index 000000000..db6574b08 --- /dev/null +++ b/tests/database/migrations/2014_10_12_300000_create_phones_table.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace Migrations +{ + + struct _2014_10_12_300000_create_phones_table : Migration + { + /*! Run the migrations. */ + void up() const override + { + Schema::create("phones", [](Blueprint &table) + { + table.id(); + + table.string(NAME); + table.timestamps(); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::dropIfExists("phones"); + } + }; + +} // namespace Migrations diff --git a/tests/models/models/torrent.hpp b/tests/models/models/torrent.hpp index 8cda9128f..7a6b2ca97 100644 --- a/tests/models/models/torrent.hpp +++ b/tests/models/models/torrent.hpp @@ -13,7 +13,7 @@ #include "models/user.hpp" #ifdef PROJECT_TINYORM_PLAYGROUND -#include "configuration.hpp" +# include "configuration.hpp" #endif namespace Models diff --git a/tests/qmake/TinyOrm.pri b/tests/qmake/TinyOrm.pri index 4a7985e4f..3b5474ad5 100644 --- a/tests/qmake/TinyOrm.pri +++ b/tests/qmake/TinyOrm.pri @@ -10,6 +10,12 @@ CONFIG(dll, dll|shared|static|staticlib): \ INCLUDEPATH += $$quote($$TINYORM_SOURCE_TREE/include/) +# TinyTom include path +# --- + +!disable_tom: \ + INCLUDEPATH += $$quote($$TINYORM_SOURCE_TREE/tom/include/) + # Link against TinyORM library # --- diff --git a/tests/qmake/common.pri b/tests/qmake/common.pri index 177bb9dc3..954cb5160 100644 --- a/tests/qmake/common.pri +++ b/tests/qmake/common.pri @@ -31,12 +31,6 @@ DEFINES += PROJECT_TINYORM_TEST CONFIG(release, debug|release): \ DEFINES *= QT_NO_DEBUG_OUTPUT -# TinyORM library defines -# --- - -# Enable code needed by tests, eg connection overriding in the Model -DEFINES *= TINYORM_TESTS_CODE - # Link against TinyORM library (also adds defines and include headers) # --- @@ -51,7 +45,7 @@ mingw: tinyRcIncludepath += $$quote($$TINYORM_SOURCE_TREE/tests/resources/) load(tiny_resource_and_manifest) tiny_resource_and_manifest($$tinyRcIncludepath, \ $$quote($$TINYORM_SOURCE_TREE/tests/resources), \ - true \ + TinyTest \ ) unset(tinyRcIncludepath) diff --git a/tom/include/include.pri b/tom/include/include.pri new file mode 100644 index 000000000..74868ca81 --- /dev/null +++ b/tom/include/include.pri @@ -0,0 +1,38 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/tom/application.hpp \ + $$PWD/tom/commands/command.hpp \ + $$PWD/tom/commands/database/wipecommand.hpp \ + $$PWD/tom/commands/environmentcommand.hpp \ + $$PWD/tom/commands/helpcommand.hpp \ + $$PWD/tom/commands/inspirecommand.hpp \ + $$PWD/tom/commands/listcommand.hpp \ + $$PWD/tom/commands/make/migrationcommand.hpp \ +# $$PWD/tom/commands/make/projectcommand.hpp \ + $$PWD/tom/commands/make/stubs/migrationstubs.hpp \ + $$PWD/tom/commands/make/stubs/projectstubs.hpp \ + $$PWD/tom/commands/migrations/freshcommand.hpp \ + $$PWD/tom/commands/migrations/installcommand.hpp \ + $$PWD/tom/commands/migrations/migratecommand.hpp \ + $$PWD/tom/commands/migrations/refreshcommand.hpp \ + $$PWD/tom/commands/migrations/resetcommand.hpp \ + $$PWD/tom/commands/migrations/rollbackcommand.hpp \ + $$PWD/tom/commands/migrations/statuscommand.hpp \ + $$PWD/tom/concerns/callscommands.hpp \ + $$PWD/tom/concerns/confirmable.hpp \ + $$PWD/tom/concerns/interactswithio.hpp \ + $$PWD/tom/concerns/printsoptions.hpp \ + $$PWD/tom/config.hpp \ + $$PWD/tom/exceptions/invalidargumenterror.hpp \ + $$PWD/tom/exceptions/invalidtemplateargumenterror.hpp \ + $$PWD/tom/exceptions/logicerror.hpp \ + $$PWD/tom/exceptions/runtimeerror.hpp \ + $$PWD/tom/exceptions/tomerror.hpp \ + $$PWD/tom/migration.hpp \ + $$PWD/tom/migrationcreator.hpp \ + $$PWD/tom/migrationrepository.hpp \ + $$PWD/tom/migrator.hpp \ + $$PWD/tom/terminal.hpp \ + $$PWD/tom/tomtypes.hpp \ + $$PWD/tom/version.hpp \ diff --git a/tom/include/tom/application.hpp b/tom/include/tom/application.hpp new file mode 100644 index 000000000..196e67497 --- /dev/null +++ b/tom/include/tom/application.hpp @@ -0,0 +1,259 @@ +#pragma once +#ifndef TOM_APPLICATION_HPP +#define TOM_APPLICATION_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include "tom/config.hpp" + +#include "tom/concerns/interactswithio.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm +{ + class DatabaseManager; +} + +namespace Tom { + +namespace Commands +{ + class Command; + class HelpCommand; + class ListCommand; +} // namespace Commands +namespace Concerns +{ + class CallsCommands; + class PrintsOptions; +} // namespace Concerns + + class Migration; + class MigrationRepository; + class Migrator; + + /*! Tom application. */ + class SHAREDLIB_EXPORT Application : public Concerns::InteractsWithIO + { + Q_DISABLE_COPY(Application) + + // To access saveOptions() + friend Commands::Command; + // To access createCommand() + friend Commands::HelpCommand; + // To access showVersion() + friend Commands::ListCommand; + // To access m_options + friend Concerns::PrintsOptions; + // To access initializeParser() and createCommand() + friend Concerns::CallsCommands; + + /*! Alias for the DatabaseManager. */ + using DatabaseManager = Orm::DatabaseManager; + + public: + /*! Constructor. */ + Application(int &argc, char **argv, std::shared_ptr db, + const char *environmentEnvName = "TOM_ENV", + QString migrationTable = QLatin1String("migrations"), + std::vector> migrations = {}); + /*! Default destructor. */ + inline ~Application() = default; + + /*! Instantiate/initialize all migration classes. */ + template + Application &migrations(); + + /*! Run the tom application. */ + int run(); + + /*! Log exception caught in the main exception handler in a current thread. */ + static void logException(const std::exception &e, bool noAnsi = false); + + /* Getters / Setters */ + /*! Get a current application environment. */ + inline const QString &environment() const noexcept; + /*! Get database manager. */ + inline DatabaseManager &db() const noexcept; + + /*! Get command-line parser. */ + inline const QCommandLineParser &parser() const noexcept; + /*! Is the application running in an interactive mode? */ + inline bool isInteractive() const noexcept; + /*! Obtain current command-line arguments. */ + QStringList arguments() const; + + /*! Set the migration repository table name. */ + inline Application &migrationTable(QString table); + +#ifdef TINYTOM_TESTS_CODE + /*! Alias for the test output row from the status command. */ + using StatusRow = std::vector; + + /*! Get result of the status command (used in auto tests). */ + static std::vector status() noexcept; + /*! Enable logic for unit testing? */ + static void enableInUnitTests() noexcept; +#endif + + protected: + /*! Alias for the commands' base class. */ + using Command = Commands::Command; + + /*! Fix m_argc/m_argv data members if the argv is empty. */ + void fixEmptyArgv(); + /*! Processes the specified function at application's normal exit. */ + void initializeAtExit() const; + + /*! Initialize the command-line parser. */ + void initializeParser(QCommandLineParser &parser); + /*! Save a copy of application options passed to the Qt's parser. */ + const QList & + saveOptions(QList &&options); + + /*! Prepend command options before common options (used by the help command). */ + QList + prependOptions(QList &&options); + + /* Run command */ + /*! Parse current application's command line. */ + void parseCommandLine(); + + /*! Initialize environment value, order: + development -> value from env. variable -> --env command-line argument. */ + void initializeEnvironment(); + /*! Obtain command name to run. */ + QString getCommandName(); + + /* Early exit during parse command-line */ + /*! Display the version information and exits. */ + Q_NORETURN void showVersion() const; + /*! Display the version information. */ + void printVersion() const; + /*! Invoke the list command. */ + Q_NORETURN void showCommandsList(int exitCode); + /*! Exit the application with post routines. */ + Q_NORETURN void exitApplication(int exitCode) const; + + /* Commands factory */ + /*! Alias for an optional command-line parser reference. */ + using OptionalParserRef = + std::optional>; + + /*! Create command by the given name. */ + std::unique_ptr + createCommand(const QString &command, OptionalParserRef parser = std::nullopt, + bool showHelp = true); + + /*! Migration repository instance. */ + std::shared_ptr createMigrationRepository(); + /*! Migrator instance. */ + std::shared_ptr createMigrator(); + + /* Others */ + /*! Get all supported commands list (used by the list command). */ + const std::vector> &createCommandsVector(); + /*! Get all supported commands' names. */ + const std::vector &commandNames() const; + /*! Get arguments list from the m_argv array. */ + QStringList prepareArguments() const; + + /*! Current application argc. */ + int &m_argc; + /*! Current application argv. */ + char **m_argv; + + /*! DatabaseManager instance. */ + std::shared_ptr m_db; + /*! The migration repository instance. */ + std::shared_ptr m_repository = nullptr; + /*! The migrator service instance. */ + std::shared_ptr m_migrator = nullptr; + + /* Only one instance can exist in the whole application, auto tests create their + own QCoreApplication instance so this has to be excluded. */ +#ifndef TINYTOM_TESTS_CODE + /*! Qt's application instance. */ + QCoreApplication m_qtApplication; + /*! Determine whether the TomApplication has its own QCoreApplication instance. */ + bool hasQtApplication = true; +#else + /*! Determine whether the TomApplication has its own QCoreApplication instance. */ + bool hasQtApplication = false; +#endif + /*! Command line parser. */ + QCommandLineParser m_parser {}; + + /*! Current environment. */ + QString m_environment = QStringLiteral("development"); + /*! Environment variable name that holds a current environment value. */ + const char *m_environmentEnvName; + /*! Migration repository table name. */ + QString m_migrationTable; + + /*! Migrations vector to process. */ + std::vector> m_migrations; + /*! Is this input means interactive? */ + bool m_interactive = true; + + /*! Application options. */ + QList m_options {}; + + /* Auto tests helpers */ +#ifdef TINYTOM_TESTS_CODE + public: + /*! Run the tom application with the given arguments (used in auto tests). */ + int runWithArguments(QStringList &&arguments); +#endif + }; + + /* public */ + + template + Application &Application::migrations() + { + m_migrations = {std::make_shared()...}; + + // Correct sort order is checked in the Migrator::createMigrationNamesMap() + + return *this; + } + + /* Getters / Setters */ + + const QString &Application::environment() const noexcept + { + return m_environment; + } + + Application::DatabaseManager &Application::db() const noexcept + { + return *m_db; + } + + const QCommandLineParser &Application::parser() const noexcept + { + return m_parser; + } + + bool Application::isInteractive() const noexcept + { + return m_interactive; + } + + Application &Application::migrationTable(QString table) + { + m_migrationTable = std::move(table); + + return *this; + } + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_APPLICATION_HPP diff --git a/tom/include/tom/commands/command.hpp b/tom/include/tom/commands/command.hpp new file mode 100644 index 000000000..b33f9c8ca --- /dev/null +++ b/tom/include/tom/commands/command.hpp @@ -0,0 +1,153 @@ +#pragma once +#ifndef TOM_COMMANDS_COMMAND_HPP +#define TOM_COMMANDS_COMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/concerns/callscommands.hpp" +#include "tom/concerns/interactswithio.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +class QCommandLineOption; + +namespace Orm +{ + class DatabaseConnection; +} + +namespace Tom::Commands +{ + + /*! Positional argument item for a console command. */ + struct PositionalArgument + { + /*! Argument name. */ + QString name; + /*! Argument description. */ + QString description; + /*! Appended to the Usage line, if empty the name is used. */ + QString syntax {}; + /*! Is argument optional? */ + bool optional = false; + /*! Argument's default value (optional argument only). */ + QString defaultValue {}; + }; + + /*! Abstract base class for the console command. */ + class Command : public Concerns::CallsCommands, + public Concerns::InteractsWithIO + { + Q_DISABLE_COPY(Command) + + public: + /*! Constructor. */ + Command(Application &application, QCommandLineParser &parser); + /*! Pure virtual destructor. */ + inline virtual ~Command() = 0; + + /*! The console command name. */ + virtual QString name() const = 0; + /*! The console command description. */ + virtual QString description() const = 0; + + /*! The console command positional arguments signature. */ + inline virtual const std::vector & + positionalArguments() const; + /*! The signature of the console command. */ + virtual QList optionsSignature() const; + + /*! The console command help. */ + inline virtual QString help() const; + + /*! Execute the console command. */ + virtual int run(); + /*! Execute the console command with the given arguments. */ + int runWithArguments(QStringList &&arguments); + + /* Getters */ + /*! Get the tom application. */ + inline Application &application() const noexcept; + /*! Determine whether a command has own positional arguments. */ + bool hasPositionalArguments() const; + /*! Determine whether a command has own options. */ + bool hasOptions() const; + + protected: + /* Getters */ + /*! Obtain passed arguments to parse (can come from three sources). */ + QStringList passedArguments() const; + + /* Parser helpers */ + /*! Check whether the option name was set in the parser. */ + bool isSet(const QString &name) const; + /*! Returns the option value found for the given option name or empty string. */ + QString value(const QString &name) const; + /*! Get a full command-line value option if value is set in the parser. */ + QString valueCmd(const QString &name, const QString &key = "") const; + /*! Get a full command-line boolean option if it's set in the parser. */ + QString boolCmd(const QString &name, const QString &key = "") const; + + /*! Check whether a positional argument at the given index was set. */ + bool hasArgument(QList::size_type index) const; + /*! Get a list of positional arguments. */ + QStringList arguments() const; + /*! Get a positional argument at the given index position. */ + QString argument(QList::size_type index) const; + /*! Get a positional argument by the given name. */ + QString argument(const QString &name) const; + + /*! Get a database connection. */ + Orm::DatabaseConnection &connection(const QString &name) const; + /*! Get a command-line parser. */ + QCommandLineParser &parser() const noexcept; + + /*! Reference to the tom application. */ + std::reference_wrapper m_application; + /*! Command line parser. */ + std::reference_wrapper m_parser; + + /*! Passed command's arguments. */ + QStringList m_arguments {}; + + /*! Alias for the QList command-line option size type. */ + using OptionsSizeType = QList::size_type; + /*! Map positional argument names to the index for obtaining values. */ + std::unordered_map m_positionalArguments {}; + + private: + /*! Initialize positional arguments map. */ + void initializePositionalArguments(); + /*! Show help if --help argument was passed. */ + void checkHelpArgument() const; + /*! Show the error wall and exit the application if the parser fails. */ + void showParserError(const QCommandLineParser &parser) const; + }; + + /* public */ + + Command::~Command() = default; + + const std::vector &Command::positionalArguments() const + { + static const std::vector cached; + + return cached; + } + + QString Command::help() const + { + return {}; + } + + Application &Command::application() const noexcept + { + return m_application; + } + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_COMMAND_HPP diff --git a/tom/include/tom/commands/database/wipecommand.hpp b/tom/include/tom/commands/database/wipecommand.hpp new file mode 100644 index 000000000..e10d986c4 --- /dev/null +++ b/tom/include/tom/commands/database/wipecommand.hpp @@ -0,0 +1,67 @@ +#pragma once +#ifndef TOM_COMMANDS_DATABASE_WIPECOMMAND_HPP +#define TOM_COMMANDS_DATABASE_WIPECOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Database +{ + + /*! Drop all tables, views, and types. */ + class WipeCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(WipeCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + WipeCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~WipeCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Drop all of the database tables. */ + void dropAllTables(const QString &database) const; + /*! Drop all of the database views. */ + void dropAllViews(const QString &database) const; + /*! Drop all of the database types. */ + void dropAllTypes(const QString &database) const; + }; + + /* public */ + + QString WipeCommand::name() const + { + return QStringLiteral("db:wipe"); + } + + QString WipeCommand::description() const + { + return QLatin1String("Drop all tables, views, and types"); + } + +} // namespace Tom::Commands::Database + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_DATABASE_WIPECOMMAND_HPP diff --git a/tom/include/tom/commands/environmentcommand.hpp b/tom/include/tom/commands/environmentcommand.hpp new file mode 100644 index 000000000..fdde2178d --- /dev/null +++ b/tom/include/tom/commands/environmentcommand.hpp @@ -0,0 +1,51 @@ +#pragma once +#ifndef TOM_COMMANDS_ENVIRONMENTCOMMAND_HPP +#define TOM_COMMANDS_ENVIRONMENTCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + + /*! Display the current environment. */ + class EnvironmentCommand : public Command + { + Q_DISABLE_COPY(EnvironmentCommand) + + public: + /*! Constructor. */ + EnvironmentCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~EnvironmentCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! Execute the console command. */ + int run() override; + }; + + /* public */ + + QString EnvironmentCommand::name() const + { + return QStringLiteral("env"); + } + + QString EnvironmentCommand::description() const + { + return QLatin1String("Display the current framework environment"); + } + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_ENVIRONMENTCOMMAND_HPP diff --git a/tom/include/tom/commands/helpcommand.hpp b/tom/include/tom/commands/helpcommand.hpp new file mode 100644 index 000000000..f40a14f49 --- /dev/null +++ b/tom/include/tom/commands/helpcommand.hpp @@ -0,0 +1,85 @@ +#pragma once +#ifndef TOM_COMMANDS_HELPCOMMAND_HPP +#define TOM_COMMANDS_HELPCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/printsoptions.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + + /*! Display help for a command. */ + class HelpCommand : public Command, + public Concerns::PrintsOptions + { + Q_DISABLE_COPY(HelpCommand) + + public: + /*! Constructor. */ + HelpCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~HelpCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The console command positional arguments signature. */ + const std::vector &positionalArguments() const override; + /*! The console command help. */ + QString help() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Create command by the given name. */ + std::unique_ptr createCommand(const QString &name) const; + /*! Validate if all required positional arguments are after optional arguments. */ + bool validateRequiredArguments( + const std::vector &arguments) const; + + /*! Print description section. */ + void printDescriptionSection(const Command &command) const; + /*! Print usage section. */ + void printUsageSection( + const QString &commandNameArg, const Command &command, + const std::vector &arguments) const; + + /*! Print positional arguments section. */ + void printArgumentsSection( + const std::vector &arguments) const; + /*! Get max. positional argument size in all command arguments. */ + int argumentsMaxSize(const std::vector &arguments) const; + /*! Print a positional's argument default value part. */ + void printArgumentDefaultValue(const PositionalArgument &argument) const; + + /*! Print options section. */ + int printOptionsSection(const Command &command) const; + /*! Print help section. */ + void printHelpSection(const Command &command) const; + }; + + /* public */ + + QString HelpCommand::name() const + { + return QStringLiteral("help"); + } + + QString HelpCommand::description() const + { + return QLatin1String("Display help for a command"); + } + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_HELPCOMMAND_HPP diff --git a/tom/include/tom/commands/inspirecommand.hpp b/tom/include/tom/commands/inspirecommand.hpp new file mode 100644 index 000000000..1ec0e5c4b --- /dev/null +++ b/tom/include/tom/commands/inspirecommand.hpp @@ -0,0 +1,51 @@ +#pragma once +#ifndef TOM_COMMANDS_INSPIRECOMMAND_HPP +#define TOM_COMMANDS_INSPIRECOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + + /*! Display an inspiring quote. */ + class InspireCommand : public Command + { + Q_DISABLE_COPY(InspireCommand) + + public: + /*! Constructor. */ + InspireCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~InspireCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! Execute the console command. */ + int run() override; + }; + + /* public */ + + QString InspireCommand::name() const + { + return QStringLiteral("inspire"); + } + + QString InspireCommand::description() const + { + return QLatin1String("Display an inspiring quote"); + } + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_INSPIRECOMMAND_HPP diff --git a/tom/include/tom/commands/listcommand.hpp b/tom/include/tom/commands/listcommand.hpp new file mode 100644 index 000000000..05b9b6813 --- /dev/null +++ b/tom/include/tom/commands/listcommand.hpp @@ -0,0 +1,89 @@ +#pragma once +#ifndef TOM_COMMANDS_LISTCOMMAND_HPP +#define TOM_COMMANDS_LISTCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/printsoptions.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + + /*! List all available commands. */ + class ListCommand : public Command, + public Concerns::PrintsOptions + { + Q_DISABLE_COPY(ListCommand) + + public: + /*! Constructor. */ + ListCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~ListCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The console command positional arguments signature. */ + const std::vector &positionalArguments() const override; + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! The console command help. */ + QString help() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Output full commands list. */ + int full(const QString &namespaceArg); + /*! Output raw commands list and nothing else (can be consumed by scripts). */ + int raw(const QString &namespaceArg); + + /* Commands section */ + /*! Print commands section. */ + void printCommandsSection(const QString &namespaceArg, int optionsMaxSize) const; + /*! Get max. command size in all command names. */ + int commandsMaxSize(const std::vector> &commands, + int optionsMaxSize) const; + /*! Print commands to the console. */ + void printCommands(const std::vector> &commands, + int commandsMaxSize, bool hasNamespaceArg) const; + /*! Print a new namespace section. */ + void tryBeginNsSection(QString &renderingNamespace, + const QString &commandName, bool hasNamespaceArg) const; + /*! Get command's namespace from a command name. */ + QString commandNamespace(const QString &commandName) const; + + /*! Wrapper for the two methods below, helps to avoid one copy. */ + const std::vector> & + getCommandsByNamespace(const QString &name) const; + /*! Obtain all commands in the given namespace. */ + std::vector> + getCommandsInNamespace(const QString &name) const; + }; + + /* public */ + + QString ListCommand::name() const + { + return QStringLiteral("list"); + } + + QString ListCommand::description() const + { + return QLatin1String("List commands"); + } + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_LISTCOMMAND_HPP diff --git a/tom/include/tom/commands/make/migrationcommand.hpp b/tom/include/tom/commands/make/migrationcommand.hpp new file mode 100644 index 000000000..4980f5568 --- /dev/null +++ b/tom/include/tom/commands/make/migrationcommand.hpp @@ -0,0 +1,72 @@ +#pragma once +#ifndef TOM_COMMANDS_MAKE_MIGRATIONCOMMAND_HPP +#define TOM_COMMANDS_MAKE_MIGRATIONCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/migrationcreator.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make +{ + + /*! Create a new migration file. */ + class MigrationCommand : public Command + { + Q_DISABLE_COPY(MigrationCommand) + + /*! Alias for the filesystem path. */ + using fspath = std::filesystem::path; + + public: + /*! Constructor. */ + MigrationCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~MigrationCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The console command positional arguments signature. */ + const std::vector &positionalArguments() const override; + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Write the migration file to disk. */ + void writeMigration(const QString &name, const QString &table, + bool create) const; + + /*! Get migration path (either specified by '--path' option or default + location). */ + fspath getMigrationPath() const; + + /*! The migration creator instance. */ + MigrationCreator m_creator {}; + }; + + /* public */ + + QString MigrationCommand::name() const + { + return QStringLiteral("make:migration"); + } + + QString MigrationCommand::description() const + { + return QLatin1String("Create a new migration file"); + } + +} // namespace Tom::Commands::Make + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MAKE_MIGRATIONCOMMAND_HPP diff --git a/tom/include/tom/commands/make/projectcommand.hpp b/tom/include/tom/commands/make/projectcommand.hpp new file mode 100644 index 000000000..8615a6dc1 --- /dev/null +++ b/tom/include/tom/commands/make/projectcommand.hpp @@ -0,0 +1,78 @@ +#pragma once +#ifndef TOM_COMMANDS_MAKE_PROJECTCOMMAND_HPP +#define TOM_COMMANDS_MAKE_PROJECTCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +//#include "tom/migrationcreator.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make +{ + + /*! Create a new Tom application project. */ + class ProjectCommand : public Command + { + Q_DISABLE_COPY(ProjectCommand) + + /*! Alias for the filesystem path. */ +// using path = std::filesystem::path; + + public: + /*! Constructor. */ + ProjectCommand(Application &application, QCommandLineParser &parser); + /*! Virtual destructor. */ + inline ~ProjectCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The console command positional arguments signature. */ + const std::vector &positionalArguments() const override; + /*! The signature of the console command. */ + QList signature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Write the migration file to disk. */ +// void writeMigration(const QString &name, const QString &table, +// bool create) const; + /*! Get migration path (either specified by '--path' option or default + location). */ +// inline path getMigrationPath() const; + + /*! The migration creator instance. */ +// MigrationCreator m_creator {}; + }; + + /* public */ + + QString ProjectCommand::name() const + { + return QStringLiteral("make:project"); + } + + QString ProjectCommand::description() const + { + return QLatin1String("Create a new Tom application project"); + } + + /* protected */ + +// std::filesystem::path ProjectCommand::getMigrationPath() const +// { +// return path(__FILE__).parent_path().parent_path().parent_path() / "migrations"; +// } + +} // namespace Tom::Commands::Make + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MAKE_PROJECTCOMMAND_HPP diff --git a/tom/include/tom/commands/make/stubs/migrationstubs.hpp b/tom/include/tom/commands/make/stubs/migrationstubs.hpp new file mode 100644 index 000000000..cea6a10cd --- /dev/null +++ b/tom/include/tom/commands/make/stubs/migrationstubs.hpp @@ -0,0 +1,110 @@ +#pragma once +#ifndef TOM_COMMANDS_MAKE_STUBS_MIGRATIONSTUBS_HPP +#define TOM_COMMANDS_MAKE_STUBS_MIGRATIONSTUBS_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make::Stubs +{ + +/*! Empty migration stub. */ +inline const auto *const MigrationStub = R"T(#pragma once + +#include + +namespace Migrations +{ + + struct {{ class }} : Migration + { + + /*! Run the migrations. */ + void up() const override + { + // + } + + /*! Reverse the migrations. */ + void down() const override + { + // + } + }; + +} // namespace Migrations +)T"; + +/*! Migration stub for creating a new table. */ +inline const auto *const MigrationCreateStub = R"T(#pragma once + +#include + +namespace Migrations +{ + + struct {{ class }} : Migration + { + + /*! Run the migrations. */ + void up() const override + { + Schema::create("{{ table }}", [](Blueprint &table) + { + table.id(); + table.timestamps(); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::dropIfExists("{{ table }}"); + } + }; + +} // namespace Migrations +)T"; + +/*! Migration stub for updating an existing table. */ +inline const auto *const MigrationUpdateStub = R"T(#pragma once + +#include + +namespace Migrations +{ + + struct {{ class }} : Migration + { + + /*! Run the migrations. */ + void up() const override + { + Schema::table("{{ table }}", [](Blueprint &table) + { + // + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::table("{{ table }}", [](Blueprint &table) + { + // + }); + } + }; + +} // namespace Migrations +)T"; + +} // namespace Tom::Commands::Make::Stubs + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MAKE_STUBS_MIGRATIONSTUBS_HPP diff --git a/tom/include/tom/commands/make/stubs/projectstubs.hpp b/tom/include/tom/commands/make/stubs/projectstubs.hpp new file mode 100644 index 000000000..8a7e6af79 --- /dev/null +++ b/tom/include/tom/commands/make/stubs/projectstubs.hpp @@ -0,0 +1,22 @@ +#pragma once +#ifndef TOM_COMMANDS_MAKE_STUBS_PROJECTSTUBS_HPP +#define TOM_COMMANDS_MAKE_STUBS_PROJECTSTUBS_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make::Stubs +{ + +inline const auto *const XyzStub = R"T(#pragma once +)T"; + +} // namespace Tom::Commands::Make::Stubs + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MAKE_STUBS_PROJECTSTUBS_HPP diff --git a/tom/include/tom/commands/migrations/freshcommand.hpp b/tom/include/tom/commands/migrations/freshcommand.hpp new file mode 100644 index 000000000..da7a1b9f1 --- /dev/null +++ b/tom/include/tom/commands/migrations/freshcommand.hpp @@ -0,0 +1,74 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_FRESHCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_FRESHCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Rollback the last database migration/s. */ + class FreshCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(FreshCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + FreshCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~FreshCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Determine if the developer has requested database seeding. */ + bool needsSeeding() const; + /*! Run the database seeder command. */ + void runSeeder(QString &&databaseCmd) const; + + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + }; + + /* public */ + + QString FreshCommand::name() const + { + return QStringLiteral("migrate:fresh"); + } + + QString FreshCommand::description() const + { + return QLatin1String("Drop all tables and re-run all migrations"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_FRESHCOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/installcommand.hpp b/tom/include/tom/commands/migrations/installcommand.hpp new file mode 100644 index 000000000..400e4fa1b --- /dev/null +++ b/tom/include/tom/commands/migrations/installcommand.hpp @@ -0,0 +1,64 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_INSTALLCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_INSTALLCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class MigrationRepository; + +namespace Commands::Migrations +{ + + /*! Create the migration database repository. */ + class InstallCommand : public Command + { + Q_DISABLE_COPY(InstallCommand) + + public: + /*! Constructor. */ + InstallCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr repository); + /*! Virtual destructor. */ + inline ~InstallCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! The repository instance. */ + std::shared_ptr m_repository; + }; + + /* public */ + + QString InstallCommand::name() const + { + return QStringLiteral("migrate:install"); + } + + QString InstallCommand::description() const + { + return QLatin1String("Create the migration repository"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_INSTALLCOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/migratecommand.hpp b/tom/include/tom/commands/migrations/migratecommand.hpp new file mode 100644 index 000000000..456df33da --- /dev/null +++ b/tom/include/tom/commands/migrations/migratecommand.hpp @@ -0,0 +1,79 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_MIGRATECOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_MIGRATECOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Run the database migrations up/down. */ + class MigrateCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(MigrateCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + MigrateCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~MigrateCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Prepare the migration database for running. */ + void prepareDatabase() const; + /*! Load the schema state to seed the initial database schema structure. */ + void loadSchemaState() const; + + /*! Determine if the developer has requested database seeding. */ + bool needsSeeding() const; + /*! Run the database seeder command. */ + void runSeeder() const; + + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + }; + + /* public */ + + QString MigrateCommand::name() const + { + return QStringLiteral("migrate"); + } + + QString MigrateCommand::description() const + { + return QLatin1String("Run the database migrations"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_MIGRATECOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/refreshcommand.hpp b/tom/include/tom/commands/migrations/refreshcommand.hpp new file mode 100644 index 000000000..0727a5e1c --- /dev/null +++ b/tom/include/tom/commands/migrations/refreshcommand.hpp @@ -0,0 +1,74 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_REFRESHCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_REFRESHCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Rollback the last database migration. */ + class RefreshCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(RefreshCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + RefreshCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~RefreshCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! Determine if the developer has requested database seeding. */ + bool needsSeeding() const; + /*! Run the database seeder command. */ + void runSeeder(QString &&databaseCmd) const; + + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + }; + + /* public */ + + QString RefreshCommand::name() const + { + return QStringLiteral("migrate:refresh"); + } + + QString RefreshCommand::description() const + { + return QLatin1String("Rollback and re-run all migrations"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_REFRESHCOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/resetcommand.hpp b/tom/include/tom/commands/migrations/resetcommand.hpp new file mode 100644 index 000000000..5d197beac --- /dev/null +++ b/tom/include/tom/commands/migrations/resetcommand.hpp @@ -0,0 +1,69 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_RESETCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_RESETCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Rollback the last database migration. */ + class ResetCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(ResetCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + ResetCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~ResetCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + }; + + /* public */ + + QString ResetCommand::name() const + { + return QStringLiteral("migrate:reset"); + } + + QString ResetCommand::description() const + { + return QLatin1String("Rollback all database migrations"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_RESETCOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/rollbackcommand.hpp b/tom/include/tom/commands/migrations/rollbackcommand.hpp new file mode 100644 index 000000000..7950a4fd4 --- /dev/null +++ b/tom/include/tom/commands/migrations/rollbackcommand.hpp @@ -0,0 +1,69 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_ROLLBACKCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_ROLLBACKCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/commands/command.hpp" +#include "tom/concerns/confirmable.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Rollback the last database migration. */ + class RollbackCommand : public Command, + public Concerns::Confirmable + { + Q_DISABLE_COPY(RollbackCommand) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor. */ + RollbackCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~RollbackCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + + protected: + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + }; + + /* public */ + + QString RollbackCommand::name() const + { + return QStringLiteral("migrate:rollback"); + } + + QString RollbackCommand::description() const + { + return QLatin1String("Rollback the last database migration"); + } + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_ROLLBACKCOMMAND_HPP diff --git a/tom/include/tom/commands/migrations/statuscommand.hpp b/tom/include/tom/commands/migrations/statuscommand.hpp new file mode 100644 index 000000000..bc0821048 --- /dev/null +++ b/tom/include/tom/commands/migrations/statuscommand.hpp @@ -0,0 +1,118 @@ +#pragma once +#ifndef TOM_COMMANDS_MIGRATIONS_STATUSCOMMAND_HPP +#define TOM_COMMANDS_MIGRATIONS_STATUSCOMMAND_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#ifdef TINYTOM_TESTS_CODE +#include +#endif + +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + class Migrator; + +namespace Commands::Migrations +{ + + /*! Show the status of each migration. */ + class StatusCommand : public Command + { + Q_DISABLE_COPY(StatusCommand) + + /*! Alias for the tabulate cell. */ + using TableCell = InteractsWithIO::TableCell; + /*! Alias for the tabulate row. */ + using TableRow = InteractsWithIO::TableRow; + + public: + /*! Constructor. */ + StatusCommand(Application &application, QCommandLineParser &parser, + std::shared_ptr migrator); + /*! Virtual destructor. */ + inline ~StatusCommand() override = default; + + /*! The console command name. */ + inline QString name() const override; + /*! The console command description. */ + inline QString description() const override; + + /*! The signature of the console command. */ + QList optionsSignature() const override; + + /*! Execute the console command. */ + int run() override; + +#ifdef TINYTOM_TESTS_CODE + /*! Alias for the test output row. */ + using StatusRow = std::vector; + + /*! Get result of the status command (used in auto tests). */ + inline static std::vector status() noexcept; + /*! Enable logic for unit testing? */ + inline static void setInUnitTests() noexcept; +#endif + + protected: + /*! Get the status for the given ran migrations. */ + std::vector + getStatusFor(QVector &&ran, + std::map &&batches) const; +#ifdef TINYTOM_TESTS_CODE + /*! Transform migrations status for comparing in auto tests. */ + std::vector + statusForUnitTest(std::vector &&migrations) const; +#endif + + /*! The migrator service instance. */ + std::shared_ptr m_migrator; + +#ifdef TINYTOM_TESTS_CODE + /*! Result of the status command (used in auto tests). */ + T_THREAD_LOCAL + inline static std::vector m_status; + /*! Is enabled logic for unit testing? */ + T_THREAD_LOCAL + inline static auto m_inUnitTests = false; +#endif + }; + + /* public */ + + QString StatusCommand::name() const + { + return QStringLiteral("migrate:status"); + } + + QString StatusCommand::description() const + { + return QLatin1String("Show the status of each migration"); + } + +#ifdef TINYTOM_TESTS_CODE + std::vector StatusCommand::status() noexcept + { + return m_status; + } + + void StatusCommand::setInUnitTests() noexcept + { + m_inUnitTests = true; + } +#endif + +} // namespace Commands::Migrations +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_COMMANDS_MIGRATIONS_STATUSCOMMAND_HPP diff --git a/tom/include/tom/concerns/callscommands.hpp b/tom/include/tom/concerns/callscommands.hpp new file mode 100644 index 000000000..1acfd67b5 --- /dev/null +++ b/tom/include/tom/concerns/callscommands.hpp @@ -0,0 +1,67 @@ +#pragma once +#ifndef TOM_CONCERNS_CALLSCOMMANDS_HPP +#define TOM_CONCERNS_CALLSCOMMANDS_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ +namespace Commands +{ + class Command; +} + +namespace Concerns +{ + + /*! Invoke another command by name and passed arguments. */ + class CallsCommands + { + Q_DISABLE_COPY(CallsCommands) + + public: + /*! Default constructor. */ + inline CallsCommands() = default; + /*! Virtual destructor. */ + inline virtual ~CallsCommands() = default; + + /*! Call another console command. */ + inline int call(const QString &command, QStringList &&arguments = {}) const; + + protected: + /*! Run the given console command. */ + int runCommand(const QString &command, QStringList &&arguments) const; + + /*! Create command-line arguments from the given arguments. */ + QStringList + createCommandLineArguments(const QString &command, QStringList &&arguments, + QStringList &¤tArguments) const; + + /*! Get common command-line arguments from current command-line arguments. */ + QStringList getCommonArguments(QStringList &&arguments) const; + + private: + /*! Static cast *this to the Command & derived type, const version. */ + const Commands::Command &command() const; + }; + + /* public */ + + int CallsCommands::call(const QString &command, QStringList &&arguments) const + { + return runCommand(std::move(command), std::move(arguments)); + } + +} // namespace Concerns +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_CONCERNS_CALLSCOMMANDS_HPP diff --git a/tom/include/tom/concerns/confirmable.hpp b/tom/include/tom/concerns/confirmable.hpp new file mode 100644 index 000000000..f11255ec8 --- /dev/null +++ b/tom/include/tom/concerns/confirmable.hpp @@ -0,0 +1,58 @@ +#pragma once +#ifndef TOM_CONCERNS_CONFIRMABLE_HPP +#define TOM_CONCERNS_CONFIRMABLE_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ +namespace Commands +{ + class Command; +} + +namespace Concerns +{ + + /*! Prints alert and asks for the confirmation (Y/N). */ + class Confirmable + { + Q_DISABLE_COPY(Confirmable) + + /*! Alias for the Command. */ + using Command = Commands::Command; + + public: + /*! Constructor (int param. to avoid interpret it as copy ctor). */ + Confirmable(Command &command, int); + /*! Virtual destructor. */ + inline virtual ~Confirmable() = default; + + /*! Confirm before proceeding with the action (only in production environment). */ + bool confirmToProceed( + const QString &warning = QLatin1String("Application In Production!"), + const std::function &callback = nullptr) const; + + protected: + /*! Get the default confirmation callback. */ + std::function defaultConfirmCallback() const; + + /*! Reference to a command that should be confimable. */ + std::reference_wrapper m_command; + }; + +} // namespace Concerns +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_CONCERNS_CONFIRMABLE_HPP diff --git a/tom/include/tom/concerns/interactswithio.hpp b/tom/include/tom/concerns/interactswithio.hpp new file mode 100644 index 000000000..6107ba6b6 --- /dev/null +++ b/tom/include/tom/concerns/interactswithio.hpp @@ -0,0 +1,238 @@ +#pragma once +#ifndef TOM_CONCERNS_INTERACTSWITHIO_HPP +#define TOM_CONCERNS_INTERACTSWITHIO_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +class QCommandLineParser; + +namespace Tom +{ + class Application; + class Terminal; + +namespace Concerns +{ + + /*! Set of methods for the console output/input. */ + class SHAREDLIB_EXPORT InteractsWithIO + { + Q_DISABLE_COPY(InteractsWithIO) + + // To access private ctor and errorWallInternal() (used by logException()) + friend Tom::Application; + + public: + /*! Alias for the tabulate cell. */ + using TableCell = std::variant; + /*! Alias for the tabulate row. */ + using TableRow = std::vector; + + /*! Constructor. */ + explicit InteractsWithIO(const QCommandLineParser &parser); + /*! Virtual destructor. */ + virtual ~InteractsWithIO(); + + /*! Base enum for the verbosity levels. */ + enum struct Verbosity { + Quiet = 0x0001, + Normal = 0x0002, + Verbose = 0x0004, + VeryVerbose = 0x0008, + Debug = 0x0010, + }; + /*! Quiet verbosity. */ + static constexpr Verbosity Quiet = Verbosity::Quiet; + /*! Normal verbosity (default). */ + static constexpr Verbosity Normal = Verbosity::Normal; + /*! Verbose verbosity. */ + static constexpr Verbosity Verbose = Verbosity::Verbose; + /*! Very verbose verbosity. */ + static constexpr Verbosity VeryVerbose = Verbosity::VeryVerbose; + /*! Debug verbosity. */ + static constexpr Verbosity Debug = Verbosity::Debug; + + /*! Write a string as standard output. */ + const InteractsWithIO &line(const QString &string, bool newline = true, + Verbosity verbosity = Normal, + QString &&style = "", + std::ostream &cout = std::cout) const; + /*! Write a string as note output. */ + const InteractsWithIO ¬e(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as information output. */ + const InteractsWithIO &info(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as error output. */ + const InteractsWithIO &error(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as comment output. */ + const InteractsWithIO &comment(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string in an alert box. */ + const InteractsWithIO &alert(const QString &string, + Verbosity verbosity = Normal) const; + /*! Write a string as error output (red box with a white text). */ + const InteractsWithIO &errorWall(const QString &string, + Verbosity verbosity = Normal) const; + + /*! Write a string as standard output, wide version. */ + const InteractsWithIO &wline(const QString &string, bool newline = true, + Verbosity verbosity = Normal, + QString &&style = "", + std::wostream &wcout = std::wcout) const; + /*! Write a string as note output, wide version. */ + const InteractsWithIO &wnote(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as information output, wide version. */ + const InteractsWithIO &winfo(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as error output, wide version. */ + const InteractsWithIO &werror(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string as comment output, wide version. */ + const InteractsWithIO &wcomment(const QString &string, bool newline = true, + Verbosity verbosity = Normal) const; + /*! Write a string in an alert box, wide version. */ + const InteractsWithIO &walert(const QString &string, + Verbosity verbosity = Normal) const; + /*! Write a string as error output (red box with a white text). */ + const InteractsWithIO &werrorWall(const QString &string, + Verbosity verbosity = Normal) const; + + /*! Write a blank line. */ + const InteractsWithIO &newLine(int count = 1, + Verbosity verbosity = Normal) const; + /*! Write a blank line, wide version. */ + const InteractsWithIO &newLineErr(int count = 1, + Verbosity verbosity = Normal) const; + + /*! Format input to textual table. */ + const InteractsWithIO & + table(const TableRow &headers, const std::vector &rows, + Verbosity verbosity = Normal) const; + + /*! Confirm a question with the user. */ + bool confirm(const QString &question, bool defaultAnswer = false) const; + + protected: + /*! Default constructor (used by the TomApplication, instance is initialized + later in the TomApplication::parseCommandLine()). */ + InteractsWithIO(); + /*! Initialize instance like the second constructor do, allows to create + an instance in two steps. */ + void initialize(const QCommandLineParser &parser); + + /*! Get a current verbosity level. */ + inline Verbosity verbosity() const noexcept; + /*! Is quiet verbosity level? */ + inline bool isQuietVerbosity() const noexcept; + /*! Is normal verbosity level? */ + inline bool isNormalVerbosity() const noexcept; + /*! Is verbose verbosity level? */ + inline bool isVerboseVerbosity() const noexcept; + /*! Is very verbose verbosity level? */ + inline bool isVeryVerboseVerbosity() const noexcept; + /*! Is debug verbosity level? */ + inline bool isDebugVerbosity() const noexcept; + + private: + /*! Constructor (used by TomApplication::logException()). */ + explicit InteractsWithIO(bool noAnsi); + + /*! Repalce text tags with ANSI sequences. */ + QString parseOutput(QString string, bool isAnsi = true) const; + /*! Remove tom ansi tags from the given string. */ + QString stripTags(QString string) const; + + /*! Initialize verbosity by set options in the command-line parser. */ + Verbosity initializeVerbosity(const QCommandLineParser &parser) const; + /*! Initialize ansi support by set options in the command-line parser. */ + std::optional initializeAnsi(const QCommandLineParser &parser) const; + /*! Initialize ansi support by noAnsi passed to the Application::logException. */ + std::optional initializeNoAnsi(bool noAnsi) const; + + /*! Number of the option name set on the command line (used by eg. -vvv). */ + QStringList::size_type + countSetOption(const QString &optionName, const QCommandLineParser &parser) const; + /*! Determine whether discard output by the current and the given verbosity. */ + bool dontOutput(Verbosity verbosity) const; + + /*! Should the given output use ansi? (ansi is disabled for non-tty). */ + bool isAnsiOutput(std::ostream &cout = std::cout) const; + /*! Should the given output use ansi? (ansi is disabled for non-tty), + wide version. */ + bool isAnsiWOutput(std::wostream &cout = std::wcout) const; + + /*! Write a string as error output (red box with a white text). */ + QString errorWallInternal(const QString &string) const; + + /*! Alias for the tabulate color. */ + using Color = tabulate::Color; + /*! Default tabulate table colors. */ + struct TableColors + { + Color green = Color::green; + Color red = Color::red; + }; + /*! Initialize tabulate table colors by supported ansi. */ + TableColors initializeTableColors() const; + + /*! Is this input means interactive? */ + bool m_interactive = true; + /*! Current application verbosity (defined by passed command-line options). */ + Verbosity m_verbosity = Normal; + /*! Current application ansi passed by command-line option. */ + std::optional m_ansi = std::nullopt; + /*! Describes current terminal features. */ + std::unique_ptr m_terminal; + }; + + /* protected */ + + InteractsWithIO::Verbosity InteractsWithIO::verbosity() const noexcept + { + return m_verbosity; + } + + bool InteractsWithIO::isQuietVerbosity() const noexcept + { + return m_verbosity == Quiet; + } + + bool InteractsWithIO::isNormalVerbosity() const noexcept + { + return m_verbosity == Normal; + } + + bool InteractsWithIO::isVerboseVerbosity() const noexcept + { + return m_verbosity == Verbose; + } + + bool InteractsWithIO::isVeryVerboseVerbosity() const noexcept + { + return m_verbosity == VeryVerbose; + } + + bool InteractsWithIO::isDebugVerbosity() const noexcept + { + return m_verbosity == Debug; + } + +} // namespace Concerns +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_CONCERNS_INTERACTSWITHIO_HPP diff --git a/tom/include/tom/concerns/printsoptions.hpp b/tom/include/tom/concerns/printsoptions.hpp new file mode 100644 index 000000000..f70e89bfe --- /dev/null +++ b/tom/include/tom/concerns/printsoptions.hpp @@ -0,0 +1,53 @@ +#pragma once +#ifndef TOM_CONCERNS_PRINTSOPTIONS_HPP +#define TOM_CONCERNS_PRINTSOPTIONS_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ +namespace Commands +{ + class Command; +} + +namespace Concerns +{ + + /*! Print options section. */ + class PrintsOptions + { + Q_DISABLE_COPY(PrintsOptions) + + public: + /*! Constructor (int param. to avoid interpret it as copy ctor). */ + PrintsOptions(const Commands::Command &command, int); + /*! Virtual destructor. */ + inline virtual ~PrintsOptions() = default; + + /*! Print options section. */ + int printOptionsSection(bool commonOptions) const; + + private: + /*! Get max. option size in all options. */ + int optionsMaxSize() const; + /*! Print options to the console. */ + void printOptions(int optionsMaxSize) const; + + /*! Reference to the command. */ + std::reference_wrapper m_command; + }; + +} // namespace Concerns +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_CONCERNS_PRINTSOPTIONS_HPP diff --git a/tom/include/tom/config.hpp b/tom/include/tom/config.hpp new file mode 100644 index 000000000..8e77359b6 --- /dev/null +++ b/tom/include/tom/config.hpp @@ -0,0 +1,17 @@ +#pragma once +#ifndef TOM_CONFIG_HPP +#define TOM_CONFIG_HPP + +#include +TINY_SYSTEM_HEADER + +// Check +#if defined(TINYTOM_NO_DEBUG) && defined(TINYTOM_DEBUG) +# error Both TINYTOM_DEBUG and TINYTOM_NO_DEBUG defined. +#endif +// Debug build +#if !defined(TINYTOM_NO_DEBUG) && !defined(TINYTOM_DEBUG) +# define TINYTOM_DEBUG +#endif + +#endif // TOM_CONFIG_HPP diff --git a/tom/include/tom/exceptions/invalidargumenterror.hpp b/tom/include/tom/exceptions/invalidargumenterror.hpp new file mode 100644 index 000000000..9792970cc --- /dev/null +++ b/tom/include/tom/exceptions/invalidargumenterror.hpp @@ -0,0 +1,26 @@ +#pragma once +#ifndef TOM_EXCEPTIONS_INVALIDARGUMENTERROR_HPP +#define TOM_EXCEPTIONS_INVALIDARGUMENTERROR_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/exceptions/logicerror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + + /*! Tom Invalid argument exception. */ + class InvalidArgumentError : public LogicError + { + /*! Inherit constructors. */ + using LogicError::LogicError; + }; + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_EXCEPTIONS_INVALIDARGUMENTERROR_HPP diff --git a/tom/include/tom/exceptions/invalidtemplateargumenterror.hpp b/tom/include/tom/exceptions/invalidtemplateargumenterror.hpp new file mode 100644 index 000000000..ef6842afe --- /dev/null +++ b/tom/include/tom/exceptions/invalidtemplateargumenterror.hpp @@ -0,0 +1,26 @@ +#pragma once +#ifndef TOM_EXCEPTIONS_INVALIDTEMPLATEARGUMENTERROR_HPP +#define TOM_EXCEPTIONS_INVALIDTEMPLATEARGUMENTERROR_HPP + +#include +TINY_SYSTEM_HEADER + +#include "tom/exceptions/invalidargumenterror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + + /*! Tom invalid template argument exception. */ + class InvalidTemplateArgumentError : public InvalidArgumentError + { + /*! Inherit constructors. */ + using InvalidArgumentError::InvalidArgumentError; + }; + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_EXCEPTIONS_INVALIDTEMPLATEARGUMENTERROR_HPP diff --git a/tom/include/tom/exceptions/logicerror.hpp b/tom/include/tom/exceptions/logicerror.hpp new file mode 100644 index 000000000..b0a7fd70a --- /dev/null +++ b/tom/include/tom/exceptions/logicerror.hpp @@ -0,0 +1,48 @@ +#pragma once +#ifndef TOM_EXCEPTIONS_LOGICERROR_HPP +#define TOM_EXCEPTIONS_LOGICERROR_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include "tom/exceptions/tomerror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + + /*! Tom Logic exception. */ + class LogicError : public std::logic_error, + public TomError + { + public: + /*! const char * constructor. */ + explicit LogicError(const char *message); + /*! QString constructor. */ + explicit LogicError(const QString &message); + /*! std::string constructor. */ + explicit LogicError(const std::string &message); + + /*! Return exception message as a QString. */ + inline const QString &message() const noexcept; + + protected: + /*! Exception message. */ + QString m_message = what(); + }; + + const QString &LogicError::message() const noexcept + { + return m_message; + } + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_EXCEPTIONS_LOGICERROR_HPP diff --git a/tom/include/tom/exceptions/runtimeerror.hpp b/tom/include/tom/exceptions/runtimeerror.hpp new file mode 100644 index 000000000..2ebb17782 --- /dev/null +++ b/tom/include/tom/exceptions/runtimeerror.hpp @@ -0,0 +1,48 @@ +#pragma once +#ifndef TOM_EXCEPTIONS_RUNTIMEERROR_HPP +#define TOM_EXCEPTIONS_RUNTIMEERROR_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include "tom/exceptions/tomerror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + + /*! Tom Runtime exception. */ + class RuntimeError : public std::runtime_error, + public TomError + { + public: + /*! const char * constructor. */ + explicit RuntimeError(const char *message); + /*! QString constructor. */ + explicit RuntimeError(const QString &message); + /*! std::string constructor. */ + explicit RuntimeError(const std::string &message); + + /*! Return exception message as a QString. */ + inline const QString &message() const noexcept; + + protected: + /*! Exception message. */ + QString m_message = what(); + }; + + const QString &RuntimeError::message() const noexcept + { + return m_message; + } + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_EXCEPTIONS_RUNTIMEERROR_HPP diff --git a/tom/include/tom/exceptions/tomerror.hpp b/tom/include/tom/exceptions/tomerror.hpp new file mode 100644 index 000000000..755de8801 --- /dev/null +++ b/tom/include/tom/exceptions/tomerror.hpp @@ -0,0 +1,27 @@ +#pragma once +#ifndef TOM_EXCEPTIONS_TOMERROR_HPP +#define TOM_EXCEPTIONS_TOMERROR_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + + /*! Tom exceptions tag, all Tom exceptions are derived from this class. */ + class TomError + { + public: + /*! Virtual destructor. */ + inline virtual ~TomError() = default; + }; + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_EXCEPTIONS_TOMERROR_HPP diff --git a/tom/include/tom/migration.hpp b/tom/include/tom/migration.hpp new file mode 100644 index 000000000..61863c65a --- /dev/null +++ b/tom/include/tom/migration.hpp @@ -0,0 +1,131 @@ +#pragma once +#ifndef TOM_MIGRATION_HPP +#define TOM_MIGRATION_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + + /*! Migrations base class. */ + class Migration + { + Q_DISABLE_COPY(Migration) + + public: + /*! Default constructor. */ + inline Migration() = default; + /*! Pure virtual destructor. */ + inline virtual ~Migration() = 0; + + /*! Run the migrations. */ + virtual void up() const = 0; + /*! Reverse the migrations. */ + virtual void down() const = 0; + + /*! The name of the database connection to use. */ + QString connection; + + /*! Wrapping the migration within a transaction, if supported. */ + bool withinTransaction = true; + }; + + Migration::~Migration() = default; + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +// Predefine some aliases so the user doesn't have to +namespace Migrations +{ + /*! Alias for the Schema Blueprint. */ + using TINYORM_COMMON_NAMESPACE::Orm::SchemaNs::Blueprint; // NOLINT(misc-unused-using-decls) + /*! Alias for the Tom Migration. */ + using TINYORM_COMMON_NAMESPACE::Tom::Migration; // NOLINT(misc-unused-using-decls) + /*! Alias for the Orm Schema. */ + using TINYORM_COMMON_NAMESPACE::Orm::Schema; // NOLINT(misc-unused-using-decls) + + // Aliases for the most used string constants + /*! Alias for the string constant "id". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::ID; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "name". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::NAME; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "size". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::SIZE_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "created_at". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::CREATED_AT; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "updated_at". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::UPDATED_AT; // NOLINT(misc-unused-using-decls) + + /*! Alias for the string constant "MySQL". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::MYSQL_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "PostgreSQL". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::POSTGRESQL; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "SQLite". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::SQLITE; // NOLINT(misc-unused-using-decls) + + /*! Alias for the string constant "driver". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::driver_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "host". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::host_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "port". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::port_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "database". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::database_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "schema". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::schema_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "username". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::username_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "password". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::password_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "charset". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::charset_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "collation". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::collation_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "timezone". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::timezone_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "prefix". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::prefix_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "options". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::options_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "strict". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::strict_; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "engine". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::engine_; // NOLINT(misc-unused-using-decls) + + /*! Alias for the string constant "127.0.0.1". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::H127001; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "localhost". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::LOCALHOST; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "3306". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::P3306; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "5432". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::P5432; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "root". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::ROOT; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "UTC". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::UTC; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "LOCAL". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::LOCAL; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "SYSTEM". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::SYSTEM; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "public". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::PUBLIC; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "utf8". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::UTF8; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "utf8mb4". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::UTF8MB4; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "InnoDB". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::InnoDB; // NOLINT(misc-unused-using-decls) + /*! Alias for the string constant "MyISAM". */ + using TINYORM_COMMON_NAMESPACE::Orm::Constants::MyISAM; // NOLINT(misc-unused-using-decls) + +} // namespace Migrations + +#endif // TOM_MIGRATION_HPP diff --git a/tom/include/tom/migrationcreator.hpp b/tom/include/tom/migrationcreator.hpp new file mode 100644 index 000000000..76cabba5b --- /dev/null +++ b/tom/include/tom/migrationcreator.hpp @@ -0,0 +1,65 @@ +#pragma once +#ifndef TOM_MIGRATIONCREATOR_HPP +#define TOM_MIGRATIONCREATOR_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + + /*! Migration file generator (used by the make:migration command). */ + class MigrationCreator + { + Q_DISABLE_COPY(MigrationCreator) + + /*! Alias for the filesystem path. */ + using fspath = std::filesystem::path; + + public: + /*! Default constructor. */ + inline MigrationCreator() = default; + /*! Default destructor. */ + inline ~MigrationCreator() = default; + + /*! Create a new migration at the given path. */ + fspath create(const QString &name, fspath &&migrationsPath, + const QString &table = "", bool create = false) const; + + protected: + /*! Ensure that a migration with the given name doesn't already exist. */ + void throwIfMigrationAlreadyExists(const QString &name, + const fspath &migrationsPath) const; + + /*! Get the migration stub file. */ + QString getStub(const QString &table, bool create) const; + + /*! Get the path to the stubs. */ + fspath stubPath() const; + /*! Get the full path to the migration. */ + fspath getPath(const QString &name, const fspath &path) const; + /*! Get the date prefix for the migration. */ + std::string getDatePrefix() const; + + /*! Populate the place-holders in the migration stub. */ + std::string populateStub(const QString &name, QString &&stub, + const QString &table) const; + /*! Get the class name of a migration name. */ + QString getClassName(const QString &name) const; + /*! Ensure a directory exists. */ + void ensureDirectoryExists(const fspath &path) const; + }; + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_MIGRATIONCREATOR_HPP diff --git a/tom/include/tom/migrationrepository.hpp b/tom/include/tom/migrationrepository.hpp new file mode 100644 index 000000000..c19e70e53 --- /dev/null +++ b/tom/include/tom/migrationrepository.hpp @@ -0,0 +1,94 @@ +#pragma once +#ifndef TOM_MIGRATIONREPOSITORY_HPP +#define TOM_MIGRATIONREPOSITORY_HPP + +#include + +#include + +#include "tom/tomtypes.hpp" + +class QSqlQuery; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm::Query +{ + class Builder; +} + +namespace Tom +{ + + /*! Migrations database repository. */ + class MigrationRepository + { + Q_DISABLE_COPY(MigrationRepository) + + /*! Alias for the ConnectionResolverInterface. */ + using ConnectionResolverInterface = Orm::ConnectionResolverInterface; + /*! Alias for the DatabaseConnection. */ + using DatabaseConnection = Orm::DatabaseConnection; + /*! Alias for the QueryBuilder. */ + using QueryBuilder = Orm::Query::Builder; + + public: + /*! Constructor. */ + MigrationRepository(std::shared_ptr &&resolver, + QString table); + /*! Default destructor. */ + inline ~MigrationRepository() = default; + + /*! Get the completed migrations (only migration names using pluck). */ + QVector getRanSimple() const; + /*! Get the completed migrations. */ + std::vector getRan(const QString &order) const; + /*! Get list of migrations. */ + std::vector getMigrations(int steps) const; + /*! Get the last migration batch. */ + std::vector getLast() const; + /*! Get the completed migrations with their batch numbers. */ + std::map getMigrationBatches() const; + /*! Log that a migration was run. */ + void log(const QString &file, int batch) const; + /*! Remove a migration from the log. */ + void deleteMigration(quint64 id) const; + /*! Get the next migration batch number. */ + int getNextBatchNumber() const; + /*! Get the last migration batch number. */ + int getLastBatchNumber() const; + /*! Create the migration repository data store. */ + void createRepository() const; + /*! Determine if the migration repository exists. */ + bool repositoryExists() const; + /*! Delete the migration repository data store. */ + void deleteRepository() const; + + /*! Resolve the database connection instance. */ + DatabaseConnection &getConnection() const; + /*! Set the connection name to use in the repository. */ + void setConnection(const QString &name, std::optional &&debugSql); + + protected: + /*! Get a query builder for the migration table. */ + QSharedPointer table() const; + + /*! Hydrate a vector of migration items from a raw QSqlQuery. */ + std::vector hydrateMigrations(QSqlQuery &query) const; + + /*! Set the debug sql for the current repository connection. */ + void setConnectionDebugSql(std::optional &&debugSql) const; + + /*! The database connection resolver instance. */ + std::shared_ptr m_resolver; + /*! The name of the migration table. */ + QString m_table; + /*! The name of the database connection to use. */ + QString m_connection {}; + }; + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_MIGRATIONREPOSITORY_HPP diff --git a/tom/include/tom/migrator.hpp b/tom/include/tom/migrator.hpp new file mode 100644 index 000000000..c0e15db4b --- /dev/null +++ b/tom/include/tom/migrator.hpp @@ -0,0 +1,178 @@ +#pragma once +#ifndef TOM_MIGRATOR_HPP +#define TOM_MIGRATOR_HPP + +#include +#include + +#include +#include + +#include "tom/concerns/interactswithio.hpp" +#include "tom/tomtypes.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + + class MigrationRepository; + + /*! Migration service class. */ + class Migrator : public Concerns::InteractsWithIO + { + Q_DISABLE_COPY(Migrator) + + /*! Alias for the ConnectionResolverInterface. */ + using ConnectionResolverInterface = Orm::ConnectionResolverInterface; + /*! Alias for the DatabaseConnection. */ + using DatabaseConnection = Orm::DatabaseConnection; + /*! Alias for the pretend Log. */ + using Log = Orm::Types::Log; + + public: + /*! Constructor. */ + Migrator(std::shared_ptr &&repository, + std::shared_ptr &&resolver, + const std::vector> &migrations, + const QCommandLineParser &parser); + /*! Default destructor. */ + inline ~Migrator() = default; + + /* Main migrate operations */ + /*! Migrate options. */ + struct MigrateOptions + { + /*! Dump the SQL queries that would be run. */ + bool pretend = false; + /*! Force the migrations to be run so they can be rolled back individually. */ + bool step = false; + /*! The number of migrations to be reverted. */ + int stepValue = 0; + }; + + /*! Run the pending migrations. */ + std::vector> run(MigrateOptions options) const; + /*! Rollback the last migration operation. */ + std::vector rollback(MigrateOptions options) const; + /*! Rolls all of the currently applied migrations back. */ + std::vector reset(bool pretend = false) const; + + /* Database connection related */ + /*! Execute the given callback using the given connection as the default + connection. */ + int usingConnection(QString &&name, bool debugSql, + std::function &&callback); + + /* Proxies to MigrationRepository */ + /*! Determine if the migration repository exists. */ + bool repositoryExists() const; + /*! Determine if any migrations have been run. */ + bool hasRunAnyMigrations() const; + + /* Getters / Setters */ + /*! Get the default connection name. */ + inline const QString &getConnection() const noexcept; + /*! Set the default connection name. */ + void setConnection(QString &&name, std::optional &&debugSql); + /*! Get the migration repository instance. */ + inline MigrationRepository &repository() const noexcept; + /*! Get migration names list. */ + inline const std::set &migrationNames() const noexcept; + + protected: + /* Database connection related */ + /*! Resolve the database connection instance. */ + DatabaseConnection &resolveConnection(const QString &name = "") const; + /*! Get the debug sql by the connection name. */ + std::optional getConnectionDebugSql(const QString &name) const; + + /* Migration instances lists and hashes */ + /*! Create a map that maps migration names by migrations type-id (type_index). */ + void createMigrationNamesMap(); + /*! Get a migration name by a migration type-id. */ + QString getMigrationName(const Migration &migration) const; + + /* Migrate */ + /*! Get the migration instances that have not yet run. */ + std::vector> + pendingMigrations(const QVector &ran) const; + /*! Run "up" a migration instance. */ + void runUp(const Migration &migration, int batch, bool pretend) const; + + /* Rollback */ + /*! Get the migrations for a rollback operation (used by rollback). */ + std::vector + getMigrationsForRollback(MigrateOptions options) const; + /*! Get the migrations for a rollback operation (used by reset). */ + std::vector + getMigrationsForRollback(std::vector &&ran) const; + + /*! Rollback the given migrations. */ + std::vector + rollbackMigrations(std::vector &&migrations, bool pretend) const; + /*! Run "down" a migration instance. */ + void runDown(const RollbackItem &migrationToRollback, bool pretend) const; + + /* Pretend */ + /*! Migrate type (up/down). */ + enum struct MigrateMethod { Up, Down }; + + /*! Pretend to run the migrations. */ + void pretendToRun(const Migration &migration, MigrateMethod method) const; + /*! Get all of the queries that would be run for a migration. */ + QVector getQueries(const Migration &migration, MigrateMethod method) const; + + /* Migrate up/down common */ + /*! Run a migration inside a transaction if the database supports it. */ + void runMigration(const Migration &migration, MigrateMethod method) const; + /*! Migrate by the given method (up/down). */ + void migrateByMethod(const Migration &migration, MigrateMethod method) const; + + /*! Throw if migrations passed to the TomApplication are not sorted + alphabetically. */ + void throwIfMigrationsNotSorted(const QString &previousMigrationName, + const QString &migrationName) const; + + /*! The migration repository instance. */ + std::shared_ptr m_repository; + /*! The database connection resolver instance. */ + std::shared_ptr m_resolver; + /*! The name of the database connection to use. */ + QString m_connection {}; + + /*! Reference to the migrations vector to process. */ + const std::vector> &m_migrations; + /*! Map a migration names by migrations type-id (type_index) + (used migrate, rollback, pretend). */ + std::unordered_map m_migrationNamesMap {}; + /*! Migration names list (used by status). */ + std::set m_migrationNames {}; + /*! Map a migration instances by migration names (used by reset). */ + std::unordered_map> m_migrationInstancesMap {}; + }; + + /* public */ + + const QString &Migrator::getConnection() const noexcept + { + return m_connection; + } + + MigrationRepository &Migrator::repository() const noexcept + { + return *m_repository; + } + + const std::set &Migrator::migrationNames() const noexcept + { + return m_migrationNames; + } + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_MIGRATOR_HPP + diff --git a/tom/include/tom/terminal.hpp b/tom/include/tom/terminal.hpp new file mode 100644 index 000000000..22909494a --- /dev/null +++ b/tom/include/tom/terminal.hpp @@ -0,0 +1,145 @@ +#pragma once +#ifndef TOM_TERMINAL_HPP +#define TOM_TERMINAL_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + /*! Concept for the ostream and wostream. */ + template + concept OStreamConcept = std::convertible_to || + std::convertible_to; + + /*! Describes current terminal features. */ + class Terminal + { + Q_DISABLE_COPY(Terminal) + + public: + /*! Default constructor. */ + inline Terminal() = default; + /*! Default destructor. */ + inline ~Terminal() = default; + + /*! Prepare the console terminal. */ + static void initialize(); + + /*! Supports the given output ansi colors? (ansi is disabled for non-tty). */ + bool hasColorSupport(std::ostream &cout = std::cout) const; + /*! Supports the given output ansi colors? (ansi is disabled for non-tty), + wide version. */ + bool hasWColorSupport(std::wostream &wcout = std::wcout) const; + + /*! Determines whether a file descriptor is associated with a character device. */ + bool isatty(FILE *stream) const; + + /*! Obtain the current terminal width. */ + int width(); + /*! Obtain the current terminal height. */ + int height(); + + /*! Get the cached terminal width. */ + inline int lastWidth() const noexcept; + /*! Get the cached terminal height. */ + inline int lastHeight() const noexcept; + + /*! Terminal width and height. */ + struct TerminalSize + { + /*! Visible columns. */ + int columns; + /*! Visible lines. */ + int lines; + }; + + /*! Get terminal size of the visible area. */ + TerminalSize terminalSize() const; + + private: + /*! Supports the given output ansi colors? (common logic). */ + template + bool hasColorSupportInternal(O &&cout, FILE *stream) const; + +#ifdef _WIN32 + /*! Detect if c++ ostream has enabled virtual terminal processing. */ + bool hasVt100Support(std::ostream &cout) const; + /*! Detect if c++ wostream has enabled virtual terminal processing, + wide version. */ + bool hasVt100Support(std::wostream &wcout) const; +#endif + + /* Terminal initialization */ +#ifdef _WIN32 + /*! Enable the UTF-8 console input/output character encoding. */ + static void enableUtf8ConsoleEncoding(); +#endif +#ifdef __MINGW32__ + /*! Enable the virtual terminal processing on the out/err output streams. */ + static void enableVt100Support(); +#endif + + /*! Cache for detected ansi output. */ + mutable std::unordered_map m_isAnsiOutput {}; + /*! Cache for detected ansi output, wide version. */ + mutable std::unordered_map m_isAnsiWOutput {}; + + /*! Current terminal width. */ + int m_lastWidth = 80; + /*! Current terminal height. */ + int m_lastHeight = 50; + }; + + /* public */ + + int Terminal::lastWidth() const noexcept + { + return m_lastWidth; + } + + int Terminal::lastHeight() const noexcept + { + return m_lastHeight; + } + + /* private */ + + template + bool Terminal::hasColorSupportInternal(O &&cout, FILE *stream) const + { +#ifndef _WIN32 + Q_UNUSED(cout) +#endif + // Follow https://no-color.org/ + if (qEnvironmentVariableIsSet("NO_COLOR")) + return false; + + if (qEnvironmentVariable("TERM_PROGRAM") == QLatin1String("Hyper")) + return isatty(stream); + +#ifdef _WIN32 + return isatty(stream) && + (hasVt100Support(std::forward(cout)) || + qEnvironmentVariableIsSet("ANSICON") || + qEnvironmentVariable("ConEmuANSI") == QLatin1String("ON") || + qEnvironmentVariable("TERM") == QLatin1String("xterm")); +#endif + + // Detect character device, in most cases false when the output is redirected + return isatty(stream); + } + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_TERMINAL_HPP diff --git a/tom/include/tom/tomtypes.hpp b/tom/include/tom/tomtypes.hpp new file mode 100644 index 000000000..d60100dab --- /dev/null +++ b/tom/include/tom/tomtypes.hpp @@ -0,0 +1,46 @@ +#pragma once +#ifndef TOM_TOMTYPES_HPP +#define TOM_TOMTYPES_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + + /*! Hydrated migration item from the database. */ + struct MigrationItem + { + /*! Database ID. */ + quint64 id; + /*! Migration name. */ + QString migration; + int batch; + }; + + class Migration; + + /*! Migration item used and returned from the rollback. */ + struct RollbackItem + { + /*! Database ID. */ + quint64 id; + /*! Migration name. */ + QString migrationName; + /*! Migration instance. */ + std::shared_ptr migration; + }; + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +#endif // TOM_TOMTYPES_HPP diff --git a/tom/include/tom/version.hpp b/tom/include/tom/version.hpp new file mode 100644 index 000000000..0c17e4724 --- /dev/null +++ b/tom/include/tom/version.hpp @@ -0,0 +1,47 @@ +#pragma once +#ifndef TOM_VERSION_HPP +#define TOM_VERSION_HPP + +// Excluded for the Resource compiler +#ifndef RC_INVOKED +# include +TINY_SYSTEM_HEADER +#endif + +#define TINYTOM_VERSION_MAJOR 0 +#define TINYTOM_VERSION_MINOR 1 +#define TINYTOM_VERSION_BUGFIX 0 +#define TINYTOM_VERSION_BUILD 0 +// Should be empty for stable releases, and use hypen before for SemVer compatibility! +#define TINYTOM_VERSION_STATUS "" + +// NOLINTNEXTLINE(bugprone-reserved-identifier) +#define TINYTOM__STRINGIFY(x) #x +#define TINYTOM_STRINGIFY(x) TINYTOM__STRINGIFY(x) + +#if TINYTOM_VERSION_BUILD != 0 +# define TINYTOM_PROJECT_VERSION TINYTOM_STRINGIFY( \ + TINYTOM_VERSION_MAJOR.TINYTOM_VERSION_MINOR.TINYTOM_VERSION_BUGFIX.TINYTOM_VERSION_BUILD \ + ) TINYTOM_VERSION_STATUS +#else +# define TINYTOM_PROJECT_VERSION TINYTOM_STRINGIFY( \ + TINYTOM_VERSION_MAJOR.TINYTOM_VERSION_MINOR.TINYTOM_VERSION_BUGFIX \ + ) TINYTOM_VERSION_STATUS +#endif + +/* Version Legend: + M = Major, m = minor, p = patch, t = tweak, s = status ; [] - excluded if 0 */ + +// Format - M.m.p.t (used in Windows RC file) +#define TINYTOM_FILEVERSION_STR TINYTOM_STRINGIFY( \ + TINYTOM_VERSION_MAJOR.TINYTOM_VERSION_MINOR.TINYTOM_VERSION_BUGFIX.TINYTOM_VERSION_BUILD) +// Format - M.m.p[.t]-s +#define TINYTOM_VERSION_STR TINYTOM_PROJECT_VERSION +// Format - vM.m.p[.t]-s +#define TINYTOM_VERSION_STR_2 "v" TINYTOM_PROJECT_VERSION + +/*! Version number macro, can be used to check API compatibility, format - MMmmpp. */ +#define TINYTOM_VERSION \ + (TINYTOM_VERSION_MAJOR * 10000 + TINYTOM_VERSION_MINOR * 100 + TINYTOM_VERSION_BUGFIX) + +#endif // TOM_VERSION_HPP diff --git a/tom/resources/tom.exe.manifest b/tom/resources/tom.exe.manifest new file mode 100644 index 000000000..27e74a41f --- /dev/null +++ b/tom/resources/tom.exe.manifest @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + UTF-8 + + + + + SegmentHeap + + + diff --git a/tom/resources/tom.rc.in b/tom/resources/tom.rc.in new file mode 100644 index 000000000..188e49741 --- /dev/null +++ b/tom/resources/tom.rc.in @@ -0,0 +1,55 @@ +//#pragma code_page(65001) // UTF-8 + +//IDI_ICON1 ICON "icons/@TomExample_target@.ico" + +#include +#include "tom/version.hpp" + +#define VER_FILEVERSION TINYTOM_VERSION_MAJOR,TINYTOM_VERSION_MINOR,TINYTOM_VERSION_BUGFIX,TINYTOM_VERSION_BUILD +#define VER_FILEVERSION_STR TINYTOM_FILEVERSION_STR "\0" + +#define VER_PRODUCTVERSION TINYTOM_VERSION_MAJOR,TINYTOM_VERSION_MINOR,TINYTOM_VERSION_BUGFIX,TINYTOM_VERSION_BUILD +#define VER_PRODUCTVERSION_STR TINYTOM_VERSION_STR "\0" + +#define VER_ORIGINALFILENAME_STR "$\0" + +#ifdef TINYTOM_NO_DEBUG +# define VER_DEBUG 0 +#else +# define VER_DEBUG VS_FF_DEBUG +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VER_FILEVERSION + PRODUCTVERSION VER_PRODUCTVERSION + FILEFLAGSMASK VER_DEBUG + FILEFLAGS VER_DEBUG + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE VFT2_UNKNOWN + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "Crystal Studio\0" + VALUE "FileDescription", "Tom console for TinyORM\0" + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "InternalName", "tom console\0" + VALUE "LegalCopyright", "Copyright (©) 2022 Crystal Studio\0" + VALUE "ProductName", "tom\0" + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END + END +/* End of Version info */ + +#ifdef __MINGW32__ +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "@TomExample_target@$.manifest" +#endif diff --git a/tom/src/src.pri b/tom/src/src.pri new file mode 100644 index 000000000..e8601efa5 --- /dev/null +++ b/tom/src/src.pri @@ -0,0 +1,27 @@ +SOURCES += \ + $$PWD/tom/application.cpp \ + $$PWD/tom/commands/command.cpp \ + $$PWD/tom/commands/database/wipecommand.cpp \ + $$PWD/tom/commands/environmentcommand.cpp \ + $$PWD/tom/commands/helpcommand.cpp \ + $$PWD/tom/commands/inspirecommand.cpp \ + $$PWD/tom/commands/listcommand.cpp \ + $$PWD/tom/commands/make/migrationcommand.cpp \ +# $$PWD/tom/commands/make/projectcommand.cpp \ + $$PWD/tom/commands/migrations/freshcommand.cpp \ + $$PWD/tom/commands/migrations/installcommand.cpp \ + $$PWD/tom/commands/migrations/migratecommand.cpp \ + $$PWD/tom/commands/migrations/refreshcommand.cpp \ + $$PWD/tom/commands/migrations/resetcommand.cpp \ + $$PWD/tom/commands/migrations/rollbackcommand.cpp \ + $$PWD/tom/commands/migrations/statuscommand.cpp \ + $$PWD/tom/concerns/callscommands.cpp \ + $$PWD/tom/concerns/confirmable.cpp \ + $$PWD/tom/concerns/interactswithio.cpp \ + $$PWD/tom/concerns/printsoptions.cpp \ + $$PWD/tom/exceptions/tomlogicerror.cpp \ + $$PWD/tom/exceptions/tomruntimeerror.cpp \ + $$PWD/tom/migrationcreator.cpp \ + $$PWD/tom/migrationrepository.cpp \ + $$PWD/tom/migrator.cpp \ + $$PWD/tom/terminal.cpp \ diff --git a/tom/src/tom/application.cpp b/tom/src/tom/application.cpp new file mode 100644 index 000000000..66f40c80e --- /dev/null +++ b/tom/src/tom/application.cpp @@ -0,0 +1,453 @@ +#include "tom/application.hpp" + +#include +#include + +#include +#include +#include + +#include "tom/commands/database/wipecommand.hpp" +#include "tom/commands/environmentcommand.hpp" +#include "tom/commands/helpcommand.hpp" +#include "tom/commands/inspirecommand.hpp" +#include "tom/commands/listcommand.hpp" +#include "tom/commands/make/migrationcommand.hpp" +//#include "tom/commands/make/projectcommand.hpp" +#include "tom/commands/migrations/freshcommand.hpp" +#include "tom/commands/migrations/installcommand.hpp" +#include "tom/commands/migrations/migratecommand.hpp" +#include "tom/commands/migrations/refreshcommand.hpp" +#include "tom/commands/migrations/resetcommand.hpp" +#include "tom/commands/migrations/rollbackcommand.hpp" +#include "tom/commands/migrations/statuscommand.hpp" +#include "tom/migrationrepository.hpp" +#include "tom/migrator.hpp" +#include "tom/terminal.hpp" +#ifndef TINYTOM_TESTS_CODE +# include "tom/version.hpp" +#endif + +using Orm::ConnectionResolverInterface; +using Orm::Constants::NEWLINE; + +using TypeUtils = Orm::Utils::Type; + +using Tom::Commands::Command; +using Tom::Commands::Database::WipeCommand; +using Tom::Commands::EnvironmentCommand; +using Tom::Commands::HelpCommand; +using Tom::Commands::InspireCommand; +using Tom::Commands::ListCommand; +using Tom::Commands::Make::MigrationCommand; +//using Tom::Commands::Make::ProjectCommand; +using Tom::Commands::Migrations::FreshCommand; +using Tom::Commands::Migrations::InstallCommand; +using Tom::Commands::Migrations::MigrateCommand; +using Tom::Commands::Migrations::RefreshCommand; +using Tom::Commands::Migrations::ResetCommand; +using Tom::Commands::Migrations::RollbackCommand; +using Tom::Commands::Migrations::StatusCommand; + +/*! Invoke Qt's global post routines. */ +extern void Q_DECL_IMPORT qt_call_post_routines(); + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom { + +/* Adding/removing/disabling/enabling a command, #include, using, factory in the + Application::createCommand(), add a command name to the Application::commandNames(), + update indexes in the ListCommand::getCommandsInNamespace(). */ + +/* public */ + +Application::Application(int &argc, char **argv, std::shared_ptr db, + const char *const environmentEnvName, QString migrationTable, + std::vector> migrations) + : m_argc(argc) + , m_argv(argv) + , m_db(std::move(db)) +#ifndef TINYTOM_TESTS_CODE + , m_qtApplication(argc, argv) +#endif + , m_environmentEnvName(environmentEnvName) + , m_migrationTable(std::move(migrationTable)) + , m_migrations(std::move(migrations)) +{ + // Enable UTF-8 encoding and vt100 support + Terminal::initialize(); + + // Following is not relevant in the auto test executables +#ifndef TINYTOM_TESTS_CODE + QCoreApplication::setOrganizationName("TinyORM"); + QCoreApplication::setOrganizationDomain("tinyorm.org"); + QCoreApplication::setApplicationName("tom"); + QCoreApplication::setApplicationVersion(TINYTOM_VERSION_STR); +#endif + + // Print a newline at application's normal exit +// initializeAtExit(); + + // Fix m_argc/m_argv data members if the argv is empty + fixEmptyArgv(); + + // Initialize the command-line parser + initializeParser(m_parser); +} + +int Application::run() +{ + // Process the actual command-line arguments given by the user + parseCommandLine(); + + // Ownership of a unique_ptr() + return createCommand(getCommandName())->run(); +} + +void Application::logException(const std::exception &e, const bool noAnsi) +{ + // TODO future decide how qCritical()/qFatal() really works, also regarding to the Qt Creator's settings 'Ignore first chance access violations' and similar silverqx + // TODO future alse how to correctly setup this in prod/dev envs. silverqx + auto message = QStringLiteral("Caught '%1' Exception:\n%2") + .arg(TypeUtils::classPureBasename(e, true), e.what()); + + /* Want to have this method static, downside is that the InteractsWithIO has to be + instantiated again. */ + Concerns::InteractsWithIO io(noAnsi); + + static const auto tmpl = QStringLiteral("%1%2%1").arg(NEWLINE, "%1"); + + // No-ansi output + if (noAnsi || !io.isAnsiOutput(std::cerr)) { + qCritical().nospace().noquote() << tmpl.arg(std::move(message)); + return; + } + + /* Print error wall (red box with a white text) */ + qCritical().nospace().noquote() << tmpl.arg(io.errorWallInternal(message)); +} + +QStringList Application::arguments() const +{ + // Never obtain arguments from the QCoreApplication instance in unit tests + return hasQtApplication ? QCoreApplication::arguments() + : prepareArguments(); +} + +#ifdef TINYTOM_TESTS_CODE +std::vector Application::status() noexcept +{ + return StatusCommand::status(); +} + +void Application::enableInUnitTests() noexcept +{ + StatusCommand::setInUnitTests(); +} +#endif + +/* protected */ + +void Application::fixEmptyArgv() +{ + constexpr const auto *const empty = ""; + + if (m_argc == 0 || m_argv == nullptr) { + m_argc = 0; + m_argv = const_cast(&empty); + } +} + +// CUR tom, remove? silverqx +void Application::initializeAtExit() const +{ + std::atexit([] + { + std::cout << std::endl; + }); +} + +void Application::initializeParser(QCommandLineParser &parser) +{ + parser.setApplicationDescription( + QStringLiteral("TinyORM %1").arg(TINYORM_VERSION_STR)); + + // Common options used in all commands + parser.addOptions(saveOptions({ + { "ansi", "Force ANSI output"}, + { "no-ansi", "Disable ANSI output"}, + { "env", "The environment the command should run under", + "env"}, // Value + {{"h", "help"}, "Display help for the given command. When no command " + "is given display help for the list " + "command"}, + {{"n", "no-interaction"}, "Do not ask any interactive question"}, + {{"q", "quiet"}, "Do not output any message"}, + {{"V", "version"}, "Display this application version"}, + {{"v", "verbose"}, "Increase the verbosity of messages: 1 for normal " + "output, 2 for more verbose output and 3 for debug"}, + })); +} + +const QList & +Application::saveOptions(QList &&options) +{ + return m_options = std::move(options); +} + +QList +Application::prependOptions(QList &&options) +{ + auto commonOptions = m_options; + + m_options = options; + + m_options << std::move(commonOptions); + + // Return only a new options because they are passed to the addOptions() method + return std::move(options); +} + +/* Run command */ + +void Application::parseCommandLine() +{ + /* Can not use QCommandLineParser::ParseAsPositionalArguments because I still need + to check eg. --version and --no-interaction below, it doesn't matter as everything + works just fine. */ + + /* Arguments have to be prepared manually in unit tests because is not possible + to use the QCoreApplication::arguments() method. Also no error handling needed + here as it is only some kind of pre-parse, whole command is parsed inside + the command itself. */ + m_parser.parse(arguments()); + + initializeEnvironment(); + + /* Command line arguments are parsed now, the InteractsWithIO() base class can be + initialized. Nothing bad to divide it into two steps as output to the console + is not needed until here. */ + Concerns::InteractsWithIO::initialize(m_parser); + + if (m_parser.isSet("no-interaction")) + m_interactive = false; + + if (m_parser.isSet(QStringLiteral("version"))) + showVersion(); +} + +void Application::initializeEnvironment() +{ + /*! Order is as follow, the default value is development, can be overriden by + a env. variable which name is in the m_environmentEnvName data member, highest + priority has --env command-line argument. */ + if (auto environmentCmd = m_parser.value("env"); + !environmentCmd.isEmpty() + ) + m_environment = std::move(environmentCmd); + + else if (auto environmentEnv = QString::fromUtf8(m_environmentEnvName).isEmpty() + ? "" + : qEnvironmentVariable(m_environmentEnvName); + !environmentEnv.isEmpty() + ) + m_environment = std::move(environmentEnv); +} + +QString Application::getCommandName() +{ + const auto arguments = m_parser.positionalArguments(); + + if (arguments.isEmpty()) + return {}; + + return arguments.at(0); +} + +/* Early exit during parse command-line */ + +Q_NORETURN void Application::showVersion() const +{ + printVersion(); + + exitApplication(EXIT_SUCCESS); +} + +/* Has to be divided into two methods because the printVersion() is called from + the list command. */ +void Application::printVersion() const +{ + note(QStringLiteral("TinyORM "), false); + + info(TINYORM_VERSION_STR); +} + +Q_NORETURN void Application::showCommandsList(const int exitCode) +{ + ListCommand(*this, m_parser).run(); + + exitApplication(exitCode); +} + +Q_NORETURN void Application::exitApplication(const int exitCode) const +{ + /* Invoke post routines manually, it's safe as they will not be called twice even if + the QCoreApplication's desctructor calls also this function. */ + qt_call_post_routines(); + + ::exit(exitCode); // NOLINT(concurrency-mt-unsafe) +} + +/* Commands factory */ + +std::unique_ptr +Application::createCommand(const QString &command, const OptionalParserRef parser, + const bool showHelp) +{ + // Use a custom parser if passed as the argument, needed by CallsCommands::call() + auto parserRef = parser ? *parser : std::ref(m_parser); + + if (command == QStringLiteral("db:wipe")) + return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("env")) + return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("help")) + return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("inspire")) + return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("list")) + return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("make:migration")) + return std::make_unique(*this, parserRef); + +// if (command == QStringLiteral("make:project")) +// return std::make_unique(*this, parserRef); + + if (command == QStringLiteral("migrate")) + return std::make_unique(*this, parserRef, createMigrator()); + + if (command == QStringLiteral("migrate:fresh")) + return std::make_unique(*this, parserRef, createMigrator()); + + if (command == QStringLiteral("migrate:install")) + return std::make_unique(*this, parserRef, + createMigrationRepository()); + + if (command == QStringLiteral("migrate:rollback")) + return std::make_unique(*this, parserRef, createMigrator()); + + if (command == QStringLiteral("migrate:refresh")) + return std::make_unique(*this, parserRef, createMigrator()); + + if (command == QStringLiteral("migrate:reset")) + return std::make_unique(*this, parserRef, createMigrator()); + + if (command == QStringLiteral("migrate:status")) + return std::make_unique(*this, parserRef, createMigrator()); + + // Used by the help command + if (!showHelp) + return nullptr; + + // If passed non-existent command then show all commands list + this->showCommandsList(EXIT_FAILURE); +} + +std::shared_ptr Application::createMigrationRepository() +{ + if (m_repository) + return m_repository; + + return m_repository = + std::make_shared( + std::dynamic_pointer_cast(m_db), + m_migrationTable); +} + +std::shared_ptr Application::createMigrator() +{ + if (m_migrator) + return m_migrator; + + return m_migrator = + std::make_shared( + createMigrationRepository(), + std::dynamic_pointer_cast(m_db), + m_migrations, m_parser); +} + +/* Others */ + +const std::vector> & +Application::createCommandsVector() +{ + static const std::vector> cached = [this] + { + return commandNames() + | ranges::views::transform([this](const char *const commandName) + -> std::shared_ptr + { + return createCommand(commandName); + }) + | ranges::to>>(); + }(); + + return cached; +} + +const std::vector &Application::commandNames() const +{ + // Order is important here + static const std::vector cached { + // global namespace + "env", "help", "inspire", "list", "migrate", + // db + "db:wipe", + // make + "make:migration", /*"make:project",*/ + // migrate + "migrate:fresh", "migrate:install", "migrate:refresh", "migrate:reset", + "migrate:rollback", "migrate:status", + }; + + return cached; +} + +QStringList Application::prepareArguments() const +{ + QStringList arguments; + arguments.reserve(m_argc); + + for (QStringList::size_type i = 0; i < m_argc; ++i) + arguments << QString::fromUtf8(m_argv[i]); + + return arguments; +} + +/* Auto tests helpers */ + +#ifdef TINYTOM_TESTS_CODE +/* public */ + +int Application::runWithArguments(QStringList &&arguments) +{ + // Process the actual command-line arguments given by the user + parseCommandLine(); + + // Ownership of a unique_ptr() + return createCommand(getCommandName())->runWithArguments(std::move(arguments)); +} +#endif + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE + +// CUR tom, commands I want to implement; completion, test, db:seed, schema:dump silverqx +// CUR tom, implement support for short command names, eg mig:st silverqx diff --git a/tom/src/tom/commands/command.cpp b/tom/src/tom/commands/command.cpp new file mode 100644 index 000000000..833d15d60 --- /dev/null +++ b/tom/src/tom/commands/command.cpp @@ -0,0 +1,198 @@ +#include "tom/commands/command.hpp" + +#include + +#include +#include +#include + +#include + +#include "tom/application.hpp" +#include "tom/version.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + +/* public */ + +Command::Command(Application &application, QCommandLineParser &parser) + : Concerns::InteractsWithIO(parser) + , m_application(application) + , m_parser(parser) +{} + +QList Command::optionsSignature() const +{ + return {}; +} + +int Command::run() +{ + initializePositionalArguments(); + + auto &parser = this->parser(); + + parser.clearPositionalArguments(); + + parser.addOptions(optionsSignature()); + + if (!parser.parse(passedArguments())) + showParserError(parser); + + // Show help if --help argument was passed + checkHelpArgument(); + + return EXIT_SUCCESS; +} + +int Command::runWithArguments(QStringList &&arguments) +{ + m_arguments = std::move(arguments); + + return run(); +} + +/* Getters */ + +bool Command::hasPositionalArguments() const +{ + return !positionalArguments().empty(); +} + +bool Command::hasOptions() const +{ + return !optionsSignature().isEmpty(); +} + +/* protected */ + +/* Getters */ + +QStringList Command::passedArguments() const +{ + if (!m_arguments.isEmpty()) + return m_arguments; + + // Never obtain arguments from the QCoreApplication instance in tests + return application().hasQtApplication ? QCoreApplication::arguments() + : application().prepareArguments(); +} + +/* Parser helpers */ + +bool Command::isSet(const QString &name) const +{ + return parser().isSet(name); +} + +QString Command::value(const QString &name) const +{ + return parser().value(name); +} + +QString Command::valueCmd(const QString &name, const QString &key) const +{ + if (auto value = parser().value(name); + !value.isEmpty() + ) + return QStringLiteral("--%1=%2").arg(key.isEmpty() ? name : key, + std::move(value)); + + return {}; +} + +QString Command::boolCmd(const QString &name, const QString &key) const +{ + if (!parser().isSet(name)) + return {}; + + return QStringLiteral("--%1").arg(key.isEmpty() ? name : key); +} + +bool Command::hasArgument(const QList::size_type index) const +{ + /* Has to be isNull(), an argument passed on the command line still can be an empty + value, like "", in this case it has to return a true value. */ + return !argument(index).isNull(); +} + +QStringList Command::arguments() const +{ + return parser().positionalArguments(); +} + +QString Command::argument(const QList::size_type index) const +{ + // Default value supported + return parser().positionalArguments() + .value(index, + positionalArguments().at( + static_cast(index) - 1).defaultValue); +} + +QString Command::argument(const QString &name) const +{ + // Default value supported + return argument(m_positionalArguments.at(name)); +} + +Orm::DatabaseConnection &Command::connection(const QString &name) const +{ + return application().db().connection(name); +} + +QCommandLineParser &Command::parser() const noexcept +{ + return m_parser; +} + +/* private */ + +void Command::initializePositionalArguments() +{ + const auto &arguments = positionalArguments(); + + if (arguments.empty()) + return; + + using ArgumentsSizeType = std::vector::size_type; + + m_positionalArguments = + ranges::views::zip_with([](const auto &argument, auto &&index) + -> std::pair + { + return {argument.name, index}; + }, + arguments, ranges::views::closed_iota(static_cast(1), + arguments.size()) + ) + | ranges::to(); + + // The same as above, I leave above as I want to have one example with zip_with() +// for (OptionsSizeType index = 0; const auto &argument : positionalArguments()) +// m_positionalArguments.emplace(argument.name, ++index); +} + +void Command::checkHelpArgument() const +{ + if (!isSet("help")) + return; + + call("help", {name()}); + + application().exitApplication(EXIT_SUCCESS); +} + +void Command::showParserError(const QCommandLineParser &parser) const +{ + errorWall(parser.errorText()); + + application().exitApplication(EXIT_FAILURE); +} + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/database/wipecommand.cpp b/tom/src/tom/commands/database/wipecommand.cpp new file mode 100644 index 000000000..4f707bdaa --- /dev/null +++ b/tom/src/tom/commands/database/wipecommand.cpp @@ -0,0 +1,80 @@ +#include "tom/commands/database/wipecommand.hpp" + +#include + +#include + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Database +{ + +/* public */ + +WipeCommand::WipeCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) +{} + +QList WipeCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"drop-views", "Drop all tables and views"}, + {"drop-types", "Drop all tables and types (Postgres only)"}, + {"force", "Force the operation to run when in production"}, + }; +} + +int WipeCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + const auto database = value(database_); + + if (isSet("drop-views")) { + dropAllViews(database); + + info(QLatin1String("Dropped all views successfully.")); + } + + dropAllTables(database); + + info(QLatin1String("Dropped all tables successfully.")); + + if (isSet("drop-types")) { + dropAllTypes(database); + + info(QLatin1String("Dropped all types successfully.")); + } + + return EXIT_SUCCESS; +} + +/* protected */ + +void WipeCommand::dropAllTables(const QString &database) const +{ + connection(database).getSchemaBuilder()->dropAllTables(); +} + +void WipeCommand::dropAllViews(const QString &database) const +{ + connection(database).getSchemaBuilder()->dropAllViews(); +} + +void WipeCommand::dropAllTypes(const QString &database) const +{ + connection(database).getSchemaBuilder()->dropAllTypes(); +} + +} // namespace Tom::Commands::Database + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/environmentcommand.cpp b/tom/src/tom/commands/environmentcommand.cpp new file mode 100644 index 000000000..e2c1ddca5 --- /dev/null +++ b/tom/src/tom/commands/environmentcommand.cpp @@ -0,0 +1,30 @@ +#include "tom/commands/environmentcommand.hpp" + +#include "tom/application.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + +/* public */ + +EnvironmentCommand::EnvironmentCommand(Application &application, + QCommandLineParser &parser) + : Command(application, parser) +{} + +int EnvironmentCommand::run() +{ + Command::run(); + + info(QLatin1String("Current application environment: "), false); + + comment(application().environment()); + + return EXIT_SUCCESS; +} + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/helpcommand.cpp b/tom/src/tom/commands/helpcommand.cpp new file mode 100644 index 000000000..c65929360 --- /dev/null +++ b/tom/src/tom/commands/helpcommand.cpp @@ -0,0 +1,240 @@ +#include "tom/commands/helpcommand.hpp" + +#include + +#include +#include + +#include "tom/application.hpp" + +using Orm::Constants::SPACE; + +using StringUtils = Orm::Tiny::Utils::String; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + +/* public */ + +HelpCommand::HelpCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) + , Concerns::PrintsOptions(*this, 0) +{} + +const std::vector &HelpCommand::positionalArguments() const +{ + static const std::vector cached { + {"command_name", "The command name", {}, true, "help"}, + }; + + return cached; +} + +QString HelpCommand::help() const +{ + return QLatin1String( +" The help command displays help for a given command:\n\n" +" tom help list\n\n" +" To display the list of available commands, please use the list " + "command."); +} + +int HelpCommand::run() +{ + Command::run(); + + const auto commandNameArg = argument("command_name"); + + const auto command = createCommand(commandNameArg); + const auto &arguments = command->positionalArguments(); + + if (!validateRequiredArguments(arguments)) + return EXIT_FAILURE; + + printDescriptionSection(*command); + printUsageSection(commandNameArg, *command, arguments); + + printArgumentsSection(arguments); + printOptionsSection(*command); + + printHelpSection(*command); + + return EXIT_SUCCESS; +} + +/* protected */ + +std::unique_ptr HelpCommand::createCommand(const QString &name) const +{ + auto command = application().createCommand(name, std::nullopt, false); + + if (command) + return command; + + errorWall(QLatin1String("Command '%1' is not defined.").arg(name)); + + application().exitApplication(EXIT_FAILURE); +} + +bool HelpCommand::validateRequiredArguments( + const std::vector &arguments) const +{ + // Fail on required argument after optional argument + for (std::vector::size_type i = 1; + i < arguments.size() ; ++i + ) { + const auto &left = arguments.at(i - 1); + const auto &right = arguments.at(i); + + if (left.optional && !right.optional) { + errorWall(QLatin1String("Cannot add a required argument '%1' after " + "an optional one '%2'.") + .arg(right.name, left.name)); + + return false; + } + } + + // Fail when required argument has a default value + return std::ranges::none_of(arguments, [this](const auto &argument) + { + const auto requiredWithDefault = !argument.optional && + !argument.defaultValue.isEmpty(); + + if (requiredWithDefault) + errorWall(QLatin1String("The required argument '%1' has a default value " + "'%2'.") + .arg(argument.name, argument.defaultValue)); + + return requiredWithDefault; + }); +} + +void HelpCommand::printDescriptionSection(const Command &command) const +{ + comment(QLatin1String("Description:")); + + note(QStringLiteral(" %1").arg(command.description())); +} + +void HelpCommand::printUsageSection( + const QString &commandNameArg, const Command &command, + const std::vector &arguments) const +{ + /* Everything after the option -- (double dash) is treated as positional arguments. + [] means optional, <> means positional argument. If an argument is optional, + all arguments after have to be optional. */ + + newLine(); + + comment(QLatin1String("Usage:")); + + QString usage(2, SPACE); + usage += commandNameArg; + + if (command.hasOptions()) + usage += QLatin1String(" [options]"); + + if (command.hasPositionalArguments()) { + usage += QLatin1String(" [--]"); + + auto optionalCounter = 0; + + for (const auto &argument : arguments) { + auto syntax = argument.syntax.isEmpty() ? argument.name : argument.syntax; + + if (argument.optional) { + usage += QStringLiteral(" [<%1>").arg(std::move(syntax)); + ++optionalCounter; + } + else + usage += QStringLiteral(" <%1>").arg(std::move(syntax)); + } + + usage += QString(optionalCounter, QChar(']')); + } + + note(usage); +} + +void HelpCommand::printArgumentsSection( + const std::vector &arguments) const +{ + if (arguments.empty()) + return; + + newLine(); + + comment(QLatin1String("Arguments:")); + + for (const auto argumentsMaxSize = this->argumentsMaxSize(arguments); + const auto &argument : arguments + ) { + // Compute indent + auto indent = QString(argumentsMaxSize - argument.name.size(), SPACE); + + info(QStringLiteral(" %1%2 ").arg(argument.name, std::move(indent)), false); + + note(argument.description, false); + + printArgumentDefaultValue(argument); + } +} + +int HelpCommand::argumentsMaxSize(const std::vector &arguments) const +{ + const auto it = std::ranges::max_element(arguments, std::less {}, + [](const auto &argument) + { + return argument.name.size(); + }); + + return static_cast((*it).name.size()); +} + +void HelpCommand::printArgumentDefaultValue(const PositionalArgument &argument) const +{ + // Empty default value, don't render + const auto &defaultValueRef = argument.defaultValue; + + if (defaultValueRef.isEmpty()) { + newLine(); + + return; + } + + // Quote string type + auto defaultValue = StringUtils::isNumber(defaultValueRef, true) + ? defaultValueRef + : QStringLiteral("\"%1\"").arg(defaultValueRef); + + comment(QStringLiteral(" [default: %1]").arg(std::move(defaultValue))); +} + +int HelpCommand::printOptionsSection(const Command &command) const +{ + // Prepare the command parser to show command options + parser().addOptions(application().prependOptions(command.optionsSignature())); + + return PrintsOptions::printOptionsSection(false); +} + +void HelpCommand::printHelpSection(const Command &command) const +{ + const auto help = command.help(); + + if (help.isEmpty()) + return; + + newLine(); + + comment(QLatin1String("Help:")); + + note(help); +} + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/inspirecommand.cpp b/tom/src/tom/commands/inspirecommand.cpp new file mode 100644 index 000000000..0eea13ace --- /dev/null +++ b/tom/src/tom/commands/inspirecommand.cpp @@ -0,0 +1,80 @@ +#include "tom/commands/inspirecommand.hpp" + +#include + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + +/* public */ + +InspireCommand::InspireCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) +{} + +int InspireCommand::run() +{ + Command::run(); + + static const std::vector inspires { + "Act only according to that maxim whereby you can, at the same time, will that it should become a universal law. - Immanuel Kant", + "An unexamined life is not worth living. - Socrates", + "Be present above all else. - Naval Ravikant", + "Do what you can, with what you have, where you are. - Theodore Roosevelt", + "Happiness is not something readymade. It comes from your own actions. - Dalai Lama", + "He who is contented is rich. - Laozi", + "I begin to speak only when I am certain what I will say is not better left unsaid. - Cato the Younger", + "I have not failed. I've just found 10,000 ways that won't work. - Thomas Edison", + "If you do not have a consistent goal in life, you can not live it in a consistent way. - Marcus Aurelius", + "It is never too late to be what you might have been. - George Eliot", + "It is not the man who has too little, but the man who craves more, that is poor. - Seneca", + "It is quality rather than quantity that matters. - Lucius Annaeus Seneca", + "Knowing is not enough; we must apply. Being willing is not enough; we must do. - Leonardo da Vinci", + "Let all your things have their places; let each part of your business have its time. - Benjamin Franklin", + "Live as if you were to die tomorrow. Learn as if you were to live forever. - Mahatma Gandhi", + "No surplus words or unnecessary actions. - Marcus Aurelius", + "Nothing worth having comes easy. - Theodore Roosevelt", + "Order your soul. Reduce your wants. - Augustine", + "People find pleasure in different ways. I find it in keeping my mind clear. - Marcus Aurelius", + "Simplicity is an acquired taste. - Katharine Gerould", + "Simplicity is the consequence of refined emotions. - Jean D'Alembert", + "Simplicity is the essence of happiness. - Cedric Bledsoe", + "Simplicity is the ultimate sophistication. - Leonardo da Vinci", + "Smile, breathe, and go slowly. - Thich Nhat Hanh", + "The only way to do great work is to love what you do. - Steve Jobs", + "The whole future lies in uncertainty: live immediately. - Seneca", + "Very little is needed to make a happy life. - Marcus Aurelius", + "Waste no more time arguing what a good man should be, be one. - Marcus Aurelius", + "Well begun is half done. - Aristotle", + "When there is no desire, all things are at peace. - Laozi", + "Walk as if you are kissing the Earth with your feet. - Thich Nhat Hanh", + "Because you are alive, everything is possible. - Thich Nhat Hanh", + "Breathing in, I calm body and mind. Breathing out, I smile. - Thich Nhat Hanh", + "Life is available only in the present moment. - Thich Nhat Hanh", + "The best way to take care of the future is to take care of the present moment. - Thich Nhat Hanh", + "Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less. - Marie Curie", + }; + + static const auto size = inspires.size(); + + // Obtain a random number from hardware + std::random_device rd; + // Seed the generator + std::default_random_engine generator(rd()); + // Define the range + std::uniform_int_distribution + distribute(0, static_cast(size) - 1); + + comment(inspires.at(distribute(generator))); + + // Alternative implementation +// std::srand(std::time(nullptr)); +// comment(inspires.at(std::rand() % size)); + + return EXIT_SUCCESS; +} + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/listcommand.cpp b/tom/src/tom/commands/listcommand.cpp new file mode 100644 index 000000000..c806a72ab --- /dev/null +++ b/tom/src/tom/commands/listcommand.cpp @@ -0,0 +1,236 @@ +#include "tom/commands/listcommand.hpp" + +#include + +#include +#include + +#include + +#include "tom/application.hpp" + +using Orm::Constants::COLON; +using Orm::Constants::SPACE; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands +{ + +/* public */ + +ListCommand::ListCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) + , Concerns::PrintsOptions(*this, 0) +{} + +const std::vector &ListCommand::positionalArguments() const +{ + static const std::vector cached { + {"namespace", "The namespace name", {}, true}, + }; + + return cached; +} + +QList ListCommand::optionsSignature() const +{ + return { + {"raw", "To output raw command list"}, + }; +} + +QString ListCommand::help() const +{ + return QLatin1String( +" The list command lists all commands:\n\n" +" tom list\n\n" +" You can also display the commands for a specific namespace:\n\n" +" tom list test\n\n" +" It's also possible to get raw list of commands (useful for embedding command " + "runner):\n\n" +" tom list --raw"); +} + +int ListCommand::run() +{ + Command::run(); + + const auto namespaceArg = argument("namespace"); + + return isSet("raw") ? raw(namespaceArg) : full(namespaceArg); +} + +/* protected */ + +int ListCommand::full(const QString &namespaceArg) +{ + application().printVersion(); + + newLine(); + comment(QLatin1String("Usage:")); + note(QLatin1String(" command [options] [--] [arguments]")); + + // Options section + const auto optionsMaxSize = printOptionsSection(true); + + // Commands section + printCommandsSection(namespaceArg, optionsMaxSize); + + return EXIT_SUCCESS; +} + +int ListCommand::raw(const QString &namespaceArg) +{ + const auto &commands = getCommandsByNamespace(namespaceArg); + + const auto it = std::ranges::max_element(commands, std::less {}, + [](const auto &command) + { + return command->name().size(); + }); + + const auto commandMaxSize = static_cast((*it)->name().size()); + + for (const auto &command : commands) { + auto commandName = command->name(); + auto indent = QString(commandMaxSize - commandName.size(), SPACE); + + note(QStringLiteral("%1%2 %3").arg(std::move(commandName), std::move(indent), + command->description())); + } + + return EXIT_SUCCESS; +} + +/* Commands section */ + +void ListCommand::printCommandsSection(const QString &namespaceArg, + const int optionsMaxSize) const +{ + const auto hasNamespaceArg = !namespaceArg.isNull(); + + newLine(); + + if (hasNamespaceArg) + comment(QLatin1String("Available commands for the '%1' namespace:") + .arg(namespaceArg.isEmpty() ? QLatin1String("global") : namespaceArg)); + else + comment(QLatin1String("Available commands:")); + + const auto &commands = getCommandsByNamespace(namespaceArg); + + // Get max. command size in all command names + const auto commandsMaxSize = this->commandsMaxSize(commands, optionsMaxSize); + // Print commands to the console + printCommands(commands, commandsMaxSize, hasNamespaceArg); +} + +int ListCommand::commandsMaxSize( + const std::vector> &commands, + const int optionsMaxSize) const +{ + const auto it = std::ranges::max_element(commands, std::less {}, + [](const auto &command) + { + return command->name().size(); + }); + + auto commandsMaxSize = static_cast((*it)->name().size()); + + // Align commands' description to the same level as options' description + if (commandsMaxSize < optionsMaxSize) + commandsMaxSize = optionsMaxSize; + + return commandsMaxSize; +} + +void ListCommand::printCommands( + const std::vector> &commands, + const int commandsMaxSize, const bool hasNamespaceArg) const +{ + // Currently rendering NS + QString renderingNamespace; + + for (const auto &command : commands) { + auto commandName = command->name(); + + // Begin a new namespace section + tryBeginNsSection(renderingNamespace, commandName, hasNamespaceArg); + + auto indent = QString(commandsMaxSize - commandName.size(), SPACE); + + info(QStringLiteral(" %1%2").arg(std::move(commandName), std::move(indent)), + false); + + note(QStringLiteral(" %1").arg(command->description())); + } +} + +void ListCommand::tryBeginNsSection( + QString &renderingNamespace, const QString &commandName, + const bool hasNamespaceArg) const +{ + const auto commandNamespace = this->commandNamespace(commandName); + + if (hasNamespaceArg || commandNamespace == renderingNamespace) + return; + + // Update currently rendering NS section + renderingNamespace = commandNamespace; + + comment(QStringLiteral(" %1").arg(renderingNamespace)); +} + +QString ListCommand::commandNamespace(const QString &commandName) const +{ + if (!commandName.contains(COLON)) + return {}; + + return commandName.split(COLON).at(0); +} + +const std::vector> & +ListCommand::getCommandsByNamespace(const QString &name) const +{ + // isNull() needed because still able to return global namespace for an empty string + if (name.isNull()) + return application().createCommandsVector(); + + /* This avoids one copy that would be done if commands would be returned by a value, + key thing is that it can be returned as a const reference, believe it it works. */ + static std::vector> cached; + + return cached = getCommandsInNamespace(name); +} + +std::vector> +ListCommand::getCommandsInNamespace(const QString &name) const +{ + /* First number is index where it starts (0-based), second the number where it ends + (it's like iterator's end so should point after). + Look to the Application::commandNames() to understand this indexes. + tuple is forwarded as args to the ranges::views::slice(). */ + static const std::unordered_map> cached { + {"", std::make_tuple(0, 5)}, + {"global", std::make_tuple(0, 5)}, + {"db", std::make_tuple(5, 6)}, + {"make", std::make_tuple(6, 7)}, + {"migrate", std::make_tuple(7, 13)}, + }; + + if (!cached.contains(name)) { + errorWall(QLatin1String("There are no commands defined in the '%1' namespace.") + .arg(name)); + + application().exitApplication(EXIT_FAILURE); + } + + return application().createCommandsVector() + | std::apply(ranges::views::slice, cached.at(name)) + | ranges::to>>(); +} + +} // namespace Tom::Commands + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/make/migrationcommand.cpp b/tom/src/tom/commands/make/migrationcommand.cpp new file mode 100644 index 000000000..003902aaf --- /dev/null +++ b/tom/src/tom/commands/make/migrationcommand.cpp @@ -0,0 +1,127 @@ +#include "tom/commands/make/migrationcommand.hpp" + +#include + +#include +#include + +#include "tom/exceptions/invalidargumenterror.hpp" + +namespace fs = std::filesystem; + +using fspath = std::filesystem::path; + +using Orm::Constants::NAME; + +using StringUtils = Orm::Tiny::Utils::String; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make +{ + +/* public */ + +MigrationCommand::MigrationCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) +{} + +const std::vector &MigrationCommand::positionalArguments() const +{ + static const std::vector cached { + {NAME, "The name of the migration"}, + }; + + return cached; +} + +QList MigrationCommand::optionsSignature() const +{ + return { + {"create", "The table to be created", "create"}, + {"table", "The table to migrate", "table"}, + {"path", "The location where the migration file should be created", "path"}, + {"realpath", "Indicate any provided migration file paths are pre-resolved " + "absolute paths"}, + {"fullpath", "Output the full path of the migration"}, + }; +} + +int MigrationCommand::run() +{ + Command::run(); + + /* It's possible for the developer to specify the tables to modify in this + schema operation. The developer may also specify if this table needs + to be freshly created so we can create the appropriate migrations. */ + const auto name = StringUtils::snake(argument(NAME).trimmed()); + + auto table = value(QStringLiteral("table")); + + auto createArg = value(QStringLiteral("create")); + auto create = isSet(QStringLiteral("create")); + + /* If no table was given as an option but a create option is given then we + will use the "create" option as the table name. This allows the devs + to pass a table name into this option as a short-cut for creating. */ + if (table.isEmpty() && !createArg.isEmpty()) { + table = createArg; + + create = true; + } + + // CUR tom, finish TableGuesser, when --create/--table params are not passed silverqx + /* Next, we will attempt to guess the table name if the migration name has + "create" in the name. This will allow us to provide a convenient way + of creating migrations that create new tables for the application. */ +// if (!table.isEmpty()) +// auto [table, create] = TableGuesser::guess(name); + + /* Now we are ready to write the migration out to disk. Once we've written + the migration out. */ + writeMigration(name, table, create); + + return EXIT_SUCCESS; +} + +void MigrationCommand::writeMigration(const QString &name, const QString &table, + const bool create) const +{ + auto migrationFilePath = m_creator.create(name, getMigrationPath(), table, create); + + // make_preferred() returns reference and filename() creates a new fs::path instance + const auto migrationFile = isSet("fullpath") ? migrationFilePath.make_preferred() + : migrationFilePath.filename(); + + info(QLatin1String("Created Migration: "), false); + + note(QString::fromStdString(migrationFile.string())); +} + +fspath MigrationCommand::getMigrationPath() const +{ + // Default location + if (!isSet("path")) + return fs::current_path() / "database" / "migrations"; + + auto targetPath = value("path").toStdString(); + + // The 'path' argument contains an absolute path + if (isSet("realpath")) + return fspath(std::move(targetPath)); + + // The 'path' argument contains a relative path + auto migrationsPath = fs::current_path() / std::move(targetPath); + + // Validate + if (fs::exists(migrationsPath) && !fs::is_directory(migrationsPath)) + throw Exceptions::InvalidArgumentError( + QLatin1String("Migrations path '%1' exists and it's not a directory.") + .arg(migrationsPath.c_str())); + + return migrationsPath; +} + +} // namespace Tom::Commands::Make + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/make/projectcommand.cpp b/tom/src/tom/commands/make/projectcommand.cpp new file mode 100644 index 000000000..bc50ec248 --- /dev/null +++ b/tom/src/tom/commands/make/projectcommand.cpp @@ -0,0 +1,95 @@ +#include "tom/commands/make/projectcommand.hpp" + +#include + +#include + +//using fspath = std::filesystem::path; + +using Orm::Constants::NAME; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Make +{ + +/* public */ + +ProjectCommand::ProjectCommand(Application &application, QCommandLineParser &parser) + : Command(application, parser) +{} + +const std::vector &ProjectCommand::positionalArguments() const +{ + static const std::vector cached { + {NAME, "The name of the project"}, + }; + + return cached; +} + +QList ProjectCommand::signature() const +{ + return { + {"qmake", "Create qmake project"}, + {"cmake", "Create CMake project"}, + {"tinyorm", "Crete TinyORM project"}, + {"tom", "Crete tom project"}, + {"path", "The location where the project should be created", "path"}, + {"realpath", "Indicate the path argument is an absolute path"}, + }; +} + +int ProjectCommand::run() +{ + Command::run(); + + /* It's possible for the developer to specify the tables to modify in this + schema operation. The developer may also specify if this table needs + to be freshly created so we can create the appropriate migrations. */ +// const auto name = StringUtils::snake(argument(NAME).trimmed()); + +// auto table = value(QStringLiteral("table")); + +// auto createArg = value(QStringLiteral("create")); +// auto create = isSet(QStringLiteral("create")); + +// /* If no table was given as an option but a create option is given then we +// will use the "create" option as the table name. This allows the devs +// to pass a table name into this option as a short-cut for creating. */ +// if (table.isEmpty() && !createArg.isEmpty()) { +// table = createArg; + +// create = true; +// } + +// // CUR tom, finish, when --create/--table params are not passed silverqx +// /* Next, we will attempt to guess the table name if the migration name has +// "create" in the name. This will allow us to provide a convenient way +// of creating migrations that create new tables for the application. */ +//// if (!table.isEmpty()) +//// auto [table, create] = TableGuesser::guess(name); + +// /* Now we are ready to write the migration out to disk. Once we've written +// the migration out. */ +// writeMigration(name, table, create); + + return EXIT_SUCCESS; +} + +//void ProjectCommand::writeMigration(const QString &name, const QString &table, +// const bool create) const +//{ +// auto migrationFilePath = m_creator.create(name, getMigrationPath(), table, create); + +// // make_preferred() returns reference and filename() creates a new fs::path instance +// const auto migrationFile = isSet("fullpath") ? migrationFilePath.make_preferred() +// : migrationFilePath.filename(); + +// info(QLatin1String("Created Migration: "), false) +// .note(QString::fromWCharArray(migrationFile.c_str())); +//} + +} // namespace Tom::Commands::Make + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/freshcommand.cpp b/tom/src/tom/commands/migrations/freshcommand.cpp new file mode 100644 index 000000000..16d71bc07 --- /dev/null +++ b/tom/src/tom/commands/migrations/freshcommand.cpp @@ -0,0 +1,85 @@ +#include "tom/commands/migrations/freshcommand.hpp" + +#include + +#include + +#include "tom/migrator.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +FreshCommand::FreshCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) + , m_migrator(std::move(migrator)) +{} + +QList FreshCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"drop-views", "Drop all tables and views"}, + {"drop-types", "Drop all tables and types (Postgres only)"}, + {"force", "Force the operation to run when in production"}, +// {"schema-path", "The path to a schema dump file"}, // Value +// {"seed", "Indicates if the seed task should be re-run"}, +// {"seeder", "The class name of the root seeder", "seeded"}, // Value + {"step", "Force the migrations to be run so they can be rolled back " + "individually"}, + }; +} + +int FreshCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + auto databaseCmd = valueCmd(database_); + + call("db:wipe", {databaseCmd, + QStringLiteral("--force"), + boolCmd("drop-views"), + boolCmd("drop-types")}); + + call("migrate", {databaseCmd, + QStringLiteral("--force"), + boolCmd("step"), + valueCmd("schema-path")}); + +// if (needsSeeding()) +// runSeeder(std::move(databaseCmd)); + + return EXIT_SUCCESS; +} + +/* protected */ + +bool FreshCommand::needsSeeding() const +{ + return isSet("seed") || !value("seeder").isEmpty(); +} + +void FreshCommand::runSeeder(QString &&databaseCmd) const +{ + call("db:seed", {std::move(databaseCmd), + QStringLiteral("--force"), + valueCmd("seeder", "class")}); +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/installcommand.cpp b/tom/src/tom/commands/migrations/installcommand.cpp new file mode 100644 index 000000000..d1e1ad52a --- /dev/null +++ b/tom/src/tom/commands/migrations/installcommand.cpp @@ -0,0 +1,51 @@ +#include "tom/commands/migrations/installcommand.hpp" + +#include + +#include + +#include "tom/migrationrepository.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +InstallCommand::InstallCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr repository +) + : Command(application, parser) + , m_repository(std::move(repository)) +{} + +QList InstallCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + }; +} + +int InstallCommand::run() +{ + Command::run(); + + // Database connection to use + m_repository->setConnection(value(database_), isDebugVerbosity()); + + m_repository->createRepository(); + + info(QLatin1String("Migration table created successfully.")); + + return EXIT_SUCCESS; +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE + +// FUTURE tom, add migrate:uninstall or migrate:install --uninstall silverqx diff --git a/tom/src/tom/commands/migrations/migratecommand.cpp b/tom/src/tom/commands/migrations/migratecommand.cpp new file mode 100644 index 000000000..31b1d658b --- /dev/null +++ b/tom/src/tom/commands/migrations/migratecommand.cpp @@ -0,0 +1,100 @@ +#include "tom/commands/migrations/migratecommand.hpp" + +#include + +#include + +#include "tom/migrator.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +MigrateCommand::MigrateCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) + , m_migrator(std::move(migrator)) +{} + +QList MigrateCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"force", "Force the operation to run when in production"}, + {"pretend", "Dump the SQL queries that would be run"}, + {"schema-path", "The path to a schema dump file"}, // Value +// {"seed", "Indicates if the seed task should be re-run"}, +// {"seeder", "The class name of the root seeder", "seeded"}, // Value + {"step", "Force the migrations to be run so they can be rolled back " + "individually"}, + }; +} + +int MigrateCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + return m_migrator->usingConnection(value(database_), isDebugVerbosity(), [this] + { + // Install db repository and load schema state + prepareDatabase(); + + /* Next, we will check to see if a path option has been defined. If it has + we will use the path relative to the root of this installation folder + so that migrations may be run for any path within the applications. */ + m_migrator->run({isSet("pretend"), isSet("step")}); + + /* Finally, if the "seed" option has been given, we will re-run the database + seed task to re-populate the database, which is convenient when adding + a migration and a seed at the same time, as it is only this command. */ +// if (needsSeeding() +// runSeeder(); + + return EXIT_SUCCESS; + }); +} + +/* protected */ + +void MigrateCommand::prepareDatabase() const +{ + if (!m_migrator->repositoryExists()) + call(QStringLiteral("migrate:install"), {value(database_)}); + + if (!m_migrator->hasRunAnyMigrations() && !isSet("pretend")) + loadSchemaState(); +} + +void MigrateCommand::loadSchemaState() const +{ + // CUR tom, finish load schema silverqx +} + +bool MigrateCommand::needsSeeding() const +{ + return !isSet("pretend") && (isSet("seed") || !value("seeder").isEmpty()); +} + +void MigrateCommand::runSeeder() const +{ + call("db:seed", {valueCmd(database_), + QStringLiteral("--force"), + valueCmd("seeder", "class")}); +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/refreshcommand.cpp b/tom/src/tom/commands/migrations/refreshcommand.cpp new file mode 100644 index 000000000..878d3464b --- /dev/null +++ b/tom/src/tom/commands/migrations/refreshcommand.cpp @@ -0,0 +1,87 @@ +#include "tom/commands/migrations/refreshcommand.hpp" + +#include + +#include + +#include "tom/migrator.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +RefreshCommand::RefreshCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) + , m_migrator(std::move(migrator)) +{} + +QList RefreshCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"force", "Force the operation to run when in production"}, +// {"seed", "Indicates if the seed task should be re-run"}, +// {"seeder", "The class name of the root seeder", "seeded"}, // Value + {"step", "The number of migrations to be reverted & re-run", "step"}, // Value + {"step-migrate", "Force the migrations to be run so they can be rolled back " + "individually"}, + }; +} + +int RefreshCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + auto databaseCmd = valueCmd(database_); + + /* If the "step" option is specified it means we only want to rollback a small + number of migrations before migrating again. For example, the user might + only rollback and remigrate the latest four migrations instead of all. */ + if (const auto step = value("step").toInt(); step > 0) + call("migrate:rollback", {databaseCmd, + QStringLiteral("--force"), + valueCmd("step")}); + else + call("migrate:reset", {databaseCmd, QStringLiteral("--force")}); + + call("migrate", {databaseCmd, + QStringLiteral("--force"), + boolCmd("step-migrate", "step")}); + +// if (needsSeeding()) +// runSeeder(std::move(databaseCmd)); + + return EXIT_SUCCESS; +} + +/* protected */ + +bool RefreshCommand::needsSeeding() const +{ + return isSet("seed") || !value("seeder").isEmpty(); +} + +void RefreshCommand::runSeeder(QString &&databaseCmd) const +{ + call("db:seed", {std::move(databaseCmd), + QStringLiteral("--force"), + valueCmd("seeder", "class")}); +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/resetcommand.cpp b/tom/src/tom/commands/migrations/resetcommand.cpp new file mode 100644 index 000000000..7ab4d09dd --- /dev/null +++ b/tom/src/tom/commands/migrations/resetcommand.cpp @@ -0,0 +1,61 @@ +#include "tom/commands/migrations/resetcommand.hpp" + +#include + +#include + +#include "tom/migrator.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +ResetCommand::ResetCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) + , m_migrator(std::move(migrator)) +{} + +QList ResetCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"force", "Force the operation to run when in production"}, + {"pretend", "Dump the SQL queries that would be run"}, + }; +} + +int ResetCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + return m_migrator->usingConnection(value(database_), isDebugVerbosity(), [this] + { + if (!m_migrator->repositoryExists()) { + comment(QLatin1String("Migration table not found.")); + + return EXIT_FAILURE; + } + + m_migrator->reset(isSet("pretend")); + + return EXIT_SUCCESS; + }); +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/rollbackcommand.cpp b/tom/src/tom/commands/migrations/rollbackcommand.cpp new file mode 100644 index 000000000..694b2ada0 --- /dev/null +++ b/tom/src/tom/commands/migrations/rollbackcommand.cpp @@ -0,0 +1,58 @@ +#include "tom/commands/migrations/rollbackcommand.hpp" + +#include + +#include + +#include "tom/migrator.hpp" + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +RollbackCommand::RollbackCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , Concerns::Confirmable(*this, 0) + , m_migrator(std::move(migrator)) +{} + +QList RollbackCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + {"force", "Force the operation to run when in production"}, + {"pretend", "Dump the SQL queries that would be run"}, + {"step", "The number of migrations to be reverted", "step"}, // Value + }; +} + +int RollbackCommand::run() +{ + Command::run(); + + // Ask for confirmation in the production environment + if (!confirmToProceed()) + return EXIT_FAILURE; + + // Database connection to use + return m_migrator->usingConnection(value(database_), isDebugVerbosity(), [this] + { + // Validation not needed as the toInt() returns 0 if conversion fails, like it + m_migrator->rollback({.pretend = isSet("pretend"), + .stepValue = value("step").toInt()}); + + return EXIT_SUCCESS; + }); +} + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/commands/migrations/statuscommand.cpp b/tom/src/tom/commands/migrations/statuscommand.cpp new file mode 100644 index 000000000..1ff144862 --- /dev/null +++ b/tom/src/tom/commands/migrations/statuscommand.cpp @@ -0,0 +1,133 @@ +#include "tom/commands/migrations/statuscommand.hpp" + +#include + +#include + +#include "tom/migrationrepository.hpp" +#include "tom/migrator.hpp" + +#ifdef TINYTOM_TESTS_CODE +# include +# include + +# include "tom/exceptions/runtimeerror.hpp" +#endif + +using Orm::Constants::database_; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Commands::Migrations +{ + +/* public */ + +StatusCommand::StatusCommand( + Application &application, QCommandLineParser &parser, + std::shared_ptr migrator +) + : Command(application, parser) + , m_migrator(std::move(migrator)) +{} + +QList StatusCommand::optionsSignature() const +{ + return { + {database_, "The database connection to use", database_}, // Value + }; +} + +int StatusCommand::run() +{ + Command::run(); + + // Database connection to use + return m_migrator->usingConnection(value(database_), isDebugVerbosity(), [this] + { + if (!m_migrator->repositoryExists()) { + error(QLatin1String("Migration table not found.")); + + return EXIT_FAILURE; + } + + const auto &repository = m_migrator->repository(); + + if (auto migrations = getStatusFor(repository.getRanSimple(), + repository.getMigrationBatches()); + !migrations.empty() + ) { + /* During testing save the result of a status command to the global + variable instead of outputting it, to be able to verify results. */ +#ifdef TINYTOM_TESTS_CODE + if (m_inUnitTests) + m_status = statusForUnitTest(std::move(migrations)); + else +#endif + table({"Ran?", "Migration", "Batch"}, migrations); + + return EXIT_SUCCESS; + } + +#ifdef TINYTOM_TESTS_CODE + if (m_inUnitTests) + m_status.clear(); +#endif + + error(QLatin1String("No migrations found")); + + return EXIT_SUCCESS; + }); +} + +/* protected */ + +std::vector +StatusCommand::getStatusFor(QVector &&ran, + std::map &&batches) const +{ + return m_migrator->migrationNames() + | ranges::views::transform([&ran, &batches](const auto &migration) + -> TableRow + { + auto migrationName = migration.toStdString(); + + if (ran.contains(migration)) + return {"Yes", std::move(migrationName), + batches.at(migration).toString().toStdString()}; + + return {"No", std::move(migrationName)}; + }) + | ranges::to>(); +} + +#ifdef TINYTOM_TESTS_CODE +std::vector +StatusCommand::statusForUnitTest(std::vector &&migrations) const +{ + return ranges::views::move(migrations) + | ranges::views::transform([](auto &&migration) + -> StatusRow + { + StatusRow row; + + ranges::transform(std::forward(migration), + std::back_inserter(row), [](auto &&cell) + { + if (!std::holds_alternative(cell)) + throw Exceptions::RuntimeError( + "Nested tables or type other than std::string is not supported " + "in StatusCommand::statusForTest()."); + + return std::get(std::forward(cell)); + }); + + return row; + }) + | ranges::to>(); +} +#endif + +} // namespace Tom::Commands::Migrations + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/concerns/callscommands.cpp b/tom/src/tom/concerns/callscommands.cpp new file mode 100644 index 000000000..9e539508c --- /dev/null +++ b/tom/src/tom/concerns/callscommands.cpp @@ -0,0 +1,104 @@ +#include "tom/concerns/callscommands.hpp" + +#include + +#include +#include +#include +#include + +#include "tom/application.hpp" +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Concerns +{ + +/* protected */ + +int CallsCommands::runCommand(const QString &command, QStringList &&arguments) const +{ + QCommandLineParser parser; + + auto &application = this->command().application(); + + application.initializeParser(parser); + + /* Parse needed also here because InteractiveIO ctor calls isSet(). Also no error + handling needed here, it will be handled in the Command::run(), this is only + pre-parse because of isSet() in the InteractiveIO ctor. */ + auto currentArguments = application.arguments(); + parser.parse(currentArguments); + + return application.createCommand(command, parser) + ->runWithArguments( + createCommandLineArguments(command, std::move(arguments), + std::move(currentArguments))); +} + +QStringList +CallsCommands::createCommandLineArguments( + const QString &command, QStringList &&arguments, + QStringList &¤tArguments) const +{ + // Must contain a command exe name and tom's command name + Q_ASSERT(currentArguments.size() >= 2); + + /* First create a new arguments list that starts with an executable name followed + by a command name to execute, then obtain common arguments which were passed + on the current command line, and as the last thing append passed arguments. */ + + // Absolute path of the exe name + QStringList newArguments {std::move(currentArguments[0])}; + // Command name + newArguments << std::move(command); + + // Remove a command exe name and tom's command name +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + currentArguments.remove(0, 2); +#else + currentArguments.removeFirst(); + currentArguments.removeFirst(); +#endif + + // Get common command-line arguments from the current command-line arguments + newArguments << getCommonArguments(std::move(currentArguments)); + + // Append passed arguments + std::ranges::move(ranges::actions::remove_if(std::move(arguments), + [](auto &&v) { return v.isEmpty(); }), + std::back_inserter(newArguments)); + + return newArguments; +} + +QStringList CallsCommands::getCommonArguments(QStringList &&arguments) const +{ + static const std::unordered_set allowed { + "--ansi", + "--no-ansi", + "--no-interaction", "-n", + "--quiet", "-q", + "--verbose", "-v", "-vv", "-vvv", + }; + + return ranges::views::move(arguments) + | ranges::views::filter([&allowed = allowed](auto &&argument) + { + return allowed.contains(argument); + }) + | ranges::to(); +} + +/* private */ + +const Commands::Command &CallsCommands::command() const +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + return static_cast(*this); +} + +} // namespace Tom::Concerns + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/concerns/confirmable.cpp b/tom/src/tom/concerns/confirmable.cpp new file mode 100644 index 000000000..d1b6034fb --- /dev/null +++ b/tom/src/tom/concerns/confirmable.cpp @@ -0,0 +1,63 @@ +#include "tom/concerns/confirmable.hpp" + +#include "tom/application.hpp" +#include "tom/commands/command.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Concerns +{ + +/* public */ + +Confirmable::Confirmable(Command &command, const int /*unused*/) + : m_command(command) +{} + +bool Confirmable::confirmToProceed(const QString &warning, + const std::function &callback) const +{ + const auto callback_ = callback ? callback : defaultConfirmCallback(); + + const auto shouldConfirm = std::invoke(callback_); + + // Should not confirm (not production) or the 'force' cmd. argument set + if (!shouldConfirm) + return true; + + if (const auto &parser = m_command.get().application().parser(); + parser.optionNames().contains("force") && parser.isSet("force") + ) + return true; + + // Show the alert and confirm logic + m_command.get().alert(warning); + + const auto confirmed = + m_command.get().confirm( + QLatin1String("Do you really wish to run this command?")); + + if (confirmed) + return true; + + m_command.get().comment(QLatin1String("Command Canceled!")); + + return false; +} + +/* protected */ + +std::function Confirmable::defaultConfirmCallback() const +{ + return [this] + { + const auto &environment = m_command.get().application().environment(); + + return environment == QStringLiteral("production") || + environment == QStringLiteral("prod"); + }; +} + +} // namespace Tom::Concerns + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/concerns/interactswithio.cpp b/tom/src/tom/concerns/interactswithio.cpp new file mode 100644 index 000000000..271f5416d --- /dev/null +++ b/tom/src/tom/concerns/interactswithio.cpp @@ -0,0 +1,509 @@ +#include "tom/concerns/interactswithio.hpp" + +#include + +#include +#include + +#include "tom/terminal.hpp" + +using tabulate::Color; +using tabulate::Table; + +using Orm::Constants::ASTERISK; +using Orm::Constants::NEWLINE; +using Orm::Constants::NOSPACE; +using Orm::Constants::SPACE; + +using StringUtils = Orm::Tiny::Utils::String; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Concerns +{ + +/* public */ + +InteractsWithIO::InteractsWithIO(const QCommandLineParser &parser) + : m_interactive(!parser.isSet("no-interaction")) + , m_verbosity(initializeVerbosity(parser)) + , m_ansi(initializeAnsi(parser)) + , m_terminal(std::make_unique()) +{} + +// Needed by a unique_ptr() +InteractsWithIO::~InteractsWithIO() = default; + +/* protected */ + +// Needed by a unique_ptr() +InteractsWithIO::InteractsWithIO() + : m_terminal(nullptr) +{} + +void InteractsWithIO::initialize(const QCommandLineParser &parser) +{ + m_interactive = !parser.isSet("no-interaction"); + m_verbosity = initializeVerbosity(parser); + m_ansi = initializeAnsi(parser); + m_terminal = std::make_unique(); +} + +/* private */ + +InteractsWithIO::InteractsWithIO(const bool noAnsi) + : m_ansi(initializeNoAnsi(noAnsi)) + , m_terminal(std::make_unique()) +{} + +/* public */ + +const InteractsWithIO & +InteractsWithIO::line(const QString &string, const bool newline, + const Verbosity verbosity, QString &&style, + std::ostream &cout) const +{ + if (dontOutput(verbosity)) + return *this; + + static const auto tmplStyled = QStringLiteral("<%3>%1%2"); + + auto parsedString = parseOutput(string, isAnsiOutput(cout)); + + if (style.isEmpty()) + cout << NOSPACE + .arg(std::move(parsedString), newline ? NEWLINE : "").toStdString(); + else + cout << tmplStyled + .arg(std::move(parsedString), newline ? NEWLINE : "", std::move(style)) + .toStdString(); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::note(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return line(string, newline, verbosity); +} + +const InteractsWithIO & +InteractsWithIO::info(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return line(QStringLiteral("%1").arg(string), newline, verbosity); +} + +const InteractsWithIO & +InteractsWithIO::error(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return line(QStringLiteral("%1").arg(string), newline, verbosity, + "", std::cerr); +} + +const InteractsWithIO & +InteractsWithIO::comment(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return line(QStringLiteral("%1").arg(string), newline, verbosity); +} + +const InteractsWithIO &InteractsWithIO::alert(const QString &string, + const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + const auto asterisks = ASTERISK.repeated(string.count() + 12); + + comment(asterisks); + comment(QStringLiteral("* %1 *").arg(string)); + comment(asterisks); + + newLine(); + + return *this; +} + +const InteractsWithIO &InteractsWithIO::errorWall(const QString &string, + const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + // Do not print an error wall when ansi is disabled + if (!isAnsiOutput()) + return line(string, true, verbosity, {}, std::cerr); + + static const auto tmpl = QStringLiteral("%1%2%1").arg(NEWLINE, "%1"); + + line(tmpl.arg(errorWallInternal(string)), true, verbosity, {}, std::cerr); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::wline(const QString &string, const bool newline, + const Verbosity verbosity, QString &&style, + std::wostream &wcout) const +{ + if (dontOutput(verbosity)) + return *this; + + static const auto tmplStyled = QStringLiteral("<%3>%1%2"); + + auto parsedString = parseOutput(string, isAnsiWOutput(wcout)); + + if (style.isEmpty()) + wcout << NOSPACE + .arg(std::move(parsedString), newline ? NEWLINE : "").toStdWString(); + else + wcout << tmplStyled + .arg(std::move(parsedString), newline ? NEWLINE : "", std::move(style)) + .toStdWString(); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::wnote(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return wline(string, newline, verbosity); +} + +const InteractsWithIO & +InteractsWithIO::winfo(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return wline(QStringLiteral("%1").arg(string), newline, verbosity); +} + +const InteractsWithIO & +InteractsWithIO::werror(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return wline(QStringLiteral("%1").arg(string), newline, verbosity, + "", std::wcerr); +} + +const InteractsWithIO & +InteractsWithIO::wcomment(const QString &string, const bool newline, + const Verbosity verbosity) const +{ + return wline(QStringLiteral("%1").arg(string), newline, verbosity); +} + +const InteractsWithIO &InteractsWithIO::walert(const QString &string, + const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + const auto asterisks = ASTERISK.repeated(string.count() + 12); + + wcomment(asterisks); + wcomment(QStringLiteral("* %1 *").arg(string)); + wcomment(asterisks); + + newLine(); + + return *this; +} + +const InteractsWithIO &InteractsWithIO::werrorWall(const QString &string, + const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + // Do not print an error wall when ansi is disabled + if (!isAnsiWOutput()) + return wline(string, true, verbosity, {}, std::wcerr); + + static const auto tmpl = QStringLiteral("%1%2%1").arg(NEWLINE, "%1"); + + wline(tmpl.arg(errorWallInternal(string)), true, verbosity, {}, std::wcerr); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::newLine(const int count, const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + std::cout << std::string(static_cast(count), '\n'); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::newLineErr(const int count, const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + std::cerr << std::string(static_cast(count), '\n'); + + return *this; +} + +const InteractsWithIO & +InteractsWithIO::table(const TableRow &headers, const std::vector &rows, + const Verbosity verbosity) const +{ + if (dontOutput(verbosity)) + return *this; + + Table table; + + table.add_row(headers); + + for (const auto &row : rows) + table.add_row(row); + + // Initialize tabulate table colors by supported ansi + const auto [green, red] = initializeTableColors(); + + // Format table + // Green text in header + table.row(0).format().font_color(green); + + for (std::size_t i = 1; i <= rows.size() ; ++i) { + // Remove line between rows in the tbody + if (i > 1) + table.row(i).format().hide_border_top(); + + // Ran? column : Yes - green, No - red + const auto &ran = rows.at(i - 1).at(0); + + if (!std::holds_alternative(ran)) + continue; + + if (auto &format = table.row(i).cell(0).format(); + std::get(ran) == "Yes" + ) + format.font_color(green); + else + format.font_color(red); + } + + std::cout << table << std::endl; + + return *this; +} + +bool InteractsWithIO::confirm(const QString &question, const bool defaultAnswer) const +{ + if (!m_interactive) + return defaultAnswer; + + info(QStringLiteral("%1 (yes/no) ").arg(question), false) + .comment(QStringLiteral("[%1]").arg(defaultAnswer ? QStringLiteral("yes") + : QLatin1String("no"))) + .note(QLatin1String("> "), false); + + std::wstring answerRaw; + std::wcin >> std::noskipws >> answerRaw; + + const auto answer = QString::fromStdWString(answerRaw).toLower(); + + return answer == QLatin1String("y") || answer == QLatin1String("ye") || + answer == QStringLiteral("yes"); +} + +/* private */ + +QString InteractsWithIO::parseOutput(QString string, const bool isAnsi) const +{ + // FUTURE ansi, keep track and restore previous styles, don't use ESC[0m, practically recursive parser with nested tags needed silverqx + // ansi output + if (isAnsi) + return string + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), QStringLiteral("\033[32m")) + .replace(QStringLiteral(""), QStringLiteral("\033[37;41m")) + .replace(QStringLiteral(""), QStringLiteral("\033[33m")) + .replace(QStringLiteral(""), QStringLiteral("\033[0m")) + .replace(QStringLiteral(""), QStringLiteral("\033[0m")) + .replace(QStringLiteral(""), QStringLiteral("\033[0m")); + + // no-ansi output + return stripTags(std::move(string)); +} + +QString InteractsWithIO::stripTags(QString string) const +{ + return string + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), "") + .replace(QStringLiteral(""), ""); +} + +InteractsWithIO::Verbosity +InteractsWithIO::initializeVerbosity(const QCommandLineParser &parser) const +{ + if (parser.isSet("quiet")) + return Quiet; + + const auto verboseCount = countSetOption("v", parser); + + if (verboseCount == 1) + return Verbose; + + if (verboseCount == 2) + return VeryVerbose; + + if (verboseCount >= 3) + return Debug; + + return Normal; +} + +std::optional +InteractsWithIO::initializeAnsi(const QCommandLineParser &parser) const +{ + // Ansi option has higher priority + if (parser.isSet(QLatin1String("ansi"))) + return true; + + if (parser.isSet(QLatin1String("no-ansi"))) + return false; + + return std::nullopt; +} + +std::optional +InteractsWithIO::initializeNoAnsi(const bool noAnsi) const +{ + if (noAnsi) + return false; + + return std::nullopt; +} + +QStringList::size_type +InteractsWithIO::countSetOption(const QString &optionName, + const QCommandLineParser &parser) const +{ + /* This should be in the CommandLineParser, but I will not create a wrapper class + because of one line, I don't event create a future todo task for this. */ + return static_cast( + std::ranges::count(parser.optionNames(), optionName)); +} + +bool InteractsWithIO::dontOutput(const Verbosity verbosity) const +{ + return verbosity > m_verbosity; +} + +bool InteractsWithIO::isAnsiOutput(std::ostream &cout) const +{ + // ansi was set explicitly on the command line, respect it + if (m_ansi) + return *m_ansi; + + // Instead autodetect + return m_terminal->hasColorSupport(cout); +} + +bool InteractsWithIO::isAnsiWOutput(std::wostream &wcout) const +{ + // ansi was set explicitly on the command line, respect it + if (m_ansi) + return *m_ansi; + + // Instead autodetect + return m_terminal->hasWColorSupport(wcout); +} + +namespace +{ + /*! Get max. line size after the split with the newline in all rendered lines. */ + QString::size_type getMaxLineWidth(const QStringList &lines) + { + const auto it = std::ranges::max_element(lines, std::less {}, + [](const auto &line) + { + return line.size(); + }); + + return (*it).size(); + } + +} // namespace + +QString InteractsWithIO::errorWallInternal(const QString &string) const +{ + QStringList lines; + + { + auto splitted = string.split(NEWLINE, Qt::SkipEmptyParts); + + /* Compute the max. box width */ + // Get max. line width after the split with the newline in all rendered lines + const auto maxLineWidth = std::min(m_terminal->width() - 4, + static_cast(getMaxLineWidth(splitted))); + + // Split lines by the given width + for (const auto &lineNl : splitted) + for (auto &&line : StringUtils::splitStringByWidth(lineNl, maxLineWidth)) + lines << std::move(line); + } + + QString output; + + { + // Ansi template + static const auto tmpl = QStringLiteral("\033[37;41m%1\033[0m"); + // Get final max. line width in all rendered lines (after split by the width) + const auto maxLineWidth = getMaxLineWidth(lines); + // Above/below empty line + auto emptyLine = QString(maxLineWidth + 4, SPACE); + + // Empty line above + output += tmpl.arg(emptyLine).append(NEWLINE); + + for (auto &&line : lines) { + // Prepend/append spaces + auto lineSpaced = QStringLiteral(" %1 ").arg(std::move(line)); + // Fill a line to the end with spaces + lineSpaced.append(QString(maxLineWidth - lineSpaced.size() + 4, SPACE)); + // Ansi wrap + output += tmpl.arg(std::move(lineSpaced)).append(NEWLINE); + } + + // Empty line below + output += tmpl.arg(std::move(emptyLine)); + } + + return output; +} + +InteractsWithIO::TableColors InteractsWithIO::initializeTableColors() const +{ + /* Even is I detect ansi support as true, tabulate has it's own detection logic, + it only check isatty(), it has some consequences, eg. no colors when output + is redirected and --ansi was passed to the tom application, practically all the + logic in the isAnsiOutput() will be skipped because of this tabulate internal + logic, not a big deal though. */ + if (isAnsiOutput()) + return {}; + + // Disable coloring if no-ansi + return {Color::none, Color::none}; +} + +} // namespace Tom::Concerns + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/concerns/printsoptions.cpp b/tom/src/tom/concerns/printsoptions.cpp new file mode 100644 index 000000000..fd8decebe --- /dev/null +++ b/tom/src/tom/concerns/printsoptions.cpp @@ -0,0 +1,103 @@ +#include "tom/concerns/printsoptions.hpp" + +#include + +#include "tom/application.hpp" +#include "tom/commands/command.hpp" + +using Orm::Constants::COMMA; +using Orm::Constants::SPACE; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Concerns +{ + +PrintsOptions::PrintsOptions(const Commands::Command &command, const int /*unused*/) + : m_command(command) +{} + +int PrintsOptions::printOptionsSection(const bool commonOptions) const +{ + m_command.get().newLine(); + m_command.get().comment(commonOptions ? QLatin1String("Common options:") + : QLatin1String("Options:")); + + // Get max. option size in all options + int optionsMaxSize = this->optionsMaxSize(); + // Print options to the console + printOptions(optionsMaxSize); + + return optionsMaxSize; +} + +int PrintsOptions::optionsMaxSize() const +{ + int optionsMaxSize = 0; + + for (const auto &option : m_command.get().application().m_options) { + QStringList options; + + for (const auto &names = option.names(); + const auto &name : names + ) + // Short option + if (name.size() == 1) + options << QStringLiteral("-%1") + // Custom logic for verbose option, good enough 😎 + .arg(name == QChar('v') ? QLatin1String("v|vv|vvv") + : name); + + // Long option + else + // Short and long options passed + if (names.size() == 2) + options << QStringLiteral("--%1").arg(name); + // Only long option passed + else + options << QStringLiteral(" --%1").arg(name); + + optionsMaxSize = std::max(optionsMaxSize, + static_cast(options.join(COMMA).size())); + } + + return optionsMaxSize; +} + +void PrintsOptions::printOptions(const int optionsMaxSize) const +{ + for (const auto &option : m_command.get().application().m_options) { + QStringList options; + + for (const auto &names = option.names(); + const auto &name : names + ) + // Short option + if (name.size() == 1) + options << QStringLiteral("-%1") + // Custom logic for verbose option, good enough 😎 + .arg(name == QChar('v') ? QLatin1String("v|vv|vvv") + : name); + + // Long option + else + // Short and long options passed + if (names.size() == 2) + options << QStringLiteral("--%1").arg(name); + // Only long option passed + else + options << QStringLiteral(" --%1").arg(name); + + auto joined = options.join(COMMA); + auto indent = QString(optionsMaxSize - joined.size(), SPACE); + + m_command.get().info(QStringLiteral(" %1%2 ").arg(std::move(joined), + std::move(indent)), + false) + .note(option.description()); + } +} + +} // namespace Tom::Concerns + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/exceptions/tomlogicerror.cpp b/tom/src/tom/exceptions/tomlogicerror.cpp new file mode 100644 index 000000000..3db4d8015 --- /dev/null +++ b/tom/src/tom/exceptions/tomlogicerror.cpp @@ -0,0 +1,22 @@ +#include "tom/exceptions/logicerror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + +LogicError::LogicError(const char *message) + : std::logic_error(message) +{} + +LogicError::LogicError(const QString &message) + : std::logic_error(message.toUtf8().constData()) +{} + +LogicError::LogicError(const std::string &message) + : std::logic_error(message) +{} + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/exceptions/tomruntimeerror.cpp b/tom/src/tom/exceptions/tomruntimeerror.cpp new file mode 100644 index 000000000..098f8a4e4 --- /dev/null +++ b/tom/src/tom/exceptions/tomruntimeerror.cpp @@ -0,0 +1,22 @@ +#include "tom/exceptions/runtimeerror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom::Exceptions +{ + +RuntimeError::RuntimeError(const char *message) + : std::runtime_error(message) +{} + +RuntimeError::RuntimeError(const QString &message) + : std::runtime_error(message.toUtf8().constData()) +{} + +RuntimeError::RuntimeError(const std::string &message) + : std::runtime_error(message) +{} + +} // namespace Tom::Exceptions + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/migrationcreator.cpp b/tom/src/tom/migrationcreator.cpp new file mode 100644 index 000000000..d4c51bf53 --- /dev/null +++ b/tom/src/tom/migrationcreator.cpp @@ -0,0 +1,155 @@ +#include "tom/migrationcreator.hpp" + +#include + +#include + +#include +#include + +#include "tom/commands/make/stubs/migrationstubs.hpp" +#include "tom/exceptions/invalidargumenterror.hpp" + +namespace fs = std::filesystem; + +using fspath = std::filesystem::path; + +using Orm::Constants::DOT; + +using StringUtils = Orm::Tiny::Utils::String; + +using Tom::Commands::Make::Stubs::MigrationCreateStub; +using Tom::Commands::Make::Stubs::MigrationUpdateStub; +using Tom::Commands::Make::Stubs::MigrationStub; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + +/* public */ + +fspath MigrationCreator::create(const QString &name, fspath &&migrationsPath, + const QString &table, const bool create) const +{ + auto migrationPath = getPath(name, migrationsPath); + + throwIfMigrationAlreadyExists(name, migrationsPath); + + /* First we will get the stub file for the migration, which serves as a type + of template for the migration. Once we have those we will populate the + various place-holders, and save the file. */ + auto stub = getStub(table, create); + + ensureDirectoryExists(migrationsPath); + + // Output it as binary stream to force line endings to LF + std::ofstream(migrationPath, std::ios::out | std::ios::binary) + << populateStub(name, std::move(stub), table); + + return migrationPath; +} + +/* protected */ + +namespace +{ + /*! Migration files datetime prefix format. */ + Q_GLOBAL_STATIC_WITH_ARGS(QString, DatePrefix, ("yyyy_MM_dd_HHmmss")); +} + +void MigrationCreator::throwIfMigrationAlreadyExists(const QString &name, + const fspath &migrationsPath) const +{ + // Nothing to check + if (!fs::exists(migrationsPath)) + return; + + using options = fs::directory_options; + + for (const auto &entry : + fs::directory_iterator(migrationsPath, options::skip_permission_denied) + ) { + // Check only files + if (!entry.is_regular_file()) + continue; + + // Extract migration name without datetime prefix and extension + auto entryName = QString::fromStdString(entry.path().filename().string()) + .mid(DatePrefix->size() + 1); + + entryName.truncate(entryName.lastIndexOf(DOT)); + + if (entryName == name) + throw Exceptions::InvalidArgumentError( + QStringLiteral("A '%1' migration already exists.").arg(name)); + } +} + +QString MigrationCreator::getStub(const QString &table, const bool create) const +{ + QString stub; + + if (table.isEmpty()) + stub = MigrationStub; + + else if (create) + stub = MigrationCreateStub; + + else + stub = MigrationUpdateStub; + + return stub; +} + +fspath MigrationCreator::stubPath() const +{ + return fspath(__FILE__).parent_path() / "stubs"; +} + +fspath MigrationCreator::getPath(const QString &name, const fspath &path) const +{ + return path / (getDatePrefix() + "_" + name.toStdString() + ".hpp"); +} + +std::string MigrationCreator::getDatePrefix() const +{ + return QDateTime::currentDateTime().toString(*DatePrefix).toStdString(); +} + +std::string MigrationCreator::populateStub(const QString &name, QString &&stub, + const QString &table) const +{ + const auto className = getClassName(name); + + stub.replace(QLatin1String("DummyClass"), className) + .replace(QLatin1String("{{ class }}"), className) + .replace(QLatin1String("{{class}}"), className); + + /* Here we will replace the table place-holders with the table specified by + the developer, which is useful for quickly creating a tables creation + or update migration from the console instead of typing it manually. */ + if (!table.isEmpty()) + stub.replace(QLatin1String("DummyTable"), table) + .replace(QLatin1String("{{ table }}"), table) + .replace(QLatin1String("{{table}}"), table); + + return stub.toStdString(); +} + +QString MigrationCreator::getClassName(const QString &name) const +{ + return StringUtils::studly(name); +} + +void MigrationCreator::ensureDirectoryExists(const fspath &path) const +{ + if (fs::exists(path) && fs::is_directory(path)) + return; + + fs::create_directories(path); +} + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/migrationrepository.cpp b/tom/src/tom/migrationrepository.cpp new file mode 100644 index 000000000..3f99cc067 --- /dev/null +++ b/tom/src/tom/migrationrepository.cpp @@ -0,0 +1,190 @@ +#include "tom/migrationrepository.hpp" + +#include +#include + +using Orm::Constants::ASC; +using Orm::Constants::DESC; +using Orm::Constants::GE; + +using Orm::DatabaseConnection; +using Orm::SchemaNs::Blueprint; + +using QueryBuilder = Orm::Query::Builder; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + +/* public */ + +MigrationRepository::MigrationRepository( + std::shared_ptr &&resolver, QString table +) + : m_resolver(std::move(resolver)) + , m_table(std::move(table)) +{} + +QVector MigrationRepository::getRanSimple() const +{ + // Ownership of the QSharedPointer + return table() + ->orderBy("batch", ASC) + .orderBy("migration", ASC) + .pluck("migration"); +} + +std::vector MigrationRepository::getRan(const QString &order) const +{ + // Ownership of the QSharedPointer + auto query = table() + ->orderBy("batch", order) + .orderBy("migration", order) + .get(); + + return hydrateMigrations(query); +} + +std::vector MigrationRepository::getMigrations(const int steps) const +{ + // Ownership of the QSharedPointer + auto query = table()->where("batch", GE, 1) + .orderBy("batch", DESC) + .orderBy("migration", DESC) + .take(steps) + .get(); + + return hydrateMigrations(query); +} + +std::vector MigrationRepository::getLast() const +{ + // Ownership of the QSharedPointer + auto query = table()->whereEq("batch", getLastBatchNumber()) + .orderBy("migration", DESC) + .get(); + + return hydrateMigrations(query); +} + +std::map MigrationRepository::getMigrationBatches() const +{ + // Ownership of the QSharedPointer + return table() + ->orderBy("batch", ASC) + .orderBy("migration", ASC) + .pluck("batch", "migration"); +} + +void MigrationRepository::log(const QString &file, const int batch) const +{ + // Ownership of the QSharedPointer + table()->insert({{"migration", file}, {"batch", batch}}); +} + +void MigrationRepository::deleteMigration(const quint64 id) const +{ + // Ownership of the QSharedPointer + table()->deleteRow(id); +} + +int MigrationRepository::getNextBatchNumber() const +{ + return getLastBatchNumber() + 1; +} + +int MigrationRepository::getLastBatchNumber() const +{ + // Ownership of the QSharedPointer + // Will be 0 on empty migrations table + return table()->max("batch").value(); +} + +void MigrationRepository::createRepository() const +{ + // Ownership of a unique_ptr() + const auto schema = getConnection().getSchemaBuilder(); + + /* The migrations table is responsible for keeping track of which migrations have + actually run for the application. We'll create the table to hold the migration + file paths as well as the batch ID. */ + schema->create(m_table, [](Blueprint &table) + { + table.id(); + + table.string("migration").unique(); + table.integer("batch"); + }); +} + +bool MigrationRepository::repositoryExists() const +{ + // Ownership of a unique_ptr() + const auto schema = getConnection().getSchemaBuilder(); + + return schema->hasTable(m_table); +} + +void MigrationRepository::deleteRepository() const +{ + getConnection().getSchemaBuilder()->drop(m_table); +} + +DatabaseConnection &MigrationRepository::getConnection() const +{ + return m_resolver->connection(m_connection); +} + +void MigrationRepository::setConnection(const QString &name, + std::optional &&debugSql) +{ + m_connection = name; + + if (!debugSql) + return; + + // Enable/disable showing of sql queries in the console + setConnectionDebugSql(std::move(debugSql)); +} + +/* protected */ + +QSharedPointer MigrationRepository::table() const +{ + return getConnection().table(m_table); +} + +std::vector +MigrationRepository::hydrateMigrations(QSqlQuery &query) const +{ + std::vector migration; + + while (query.next()) +#ifdef __clang__ + migration.emplace_back( + MigrationItem {query.value("id").value(), + query.value("migration").value(), + query.value("batch").value()}); +#else + migration.emplace_back(query.value("id").value(), + query.value("migration").value(), + query.value("batch").value()); +#endif + + return migration; +} + +void MigrationRepository::setConnectionDebugSql(std::optional &&debugSql) const +{ + auto &connection = getConnection(); + + if (*debugSql) + connection.enableDebugSql(); + else + connection.disableDebugSql(); +} + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/migrator.cpp b/tom/src/tom/migrator.cpp new file mode 100644 index 000000000..037d9cec4 --- /dev/null +++ b/tom/src/tom/migrator.cpp @@ -0,0 +1,415 @@ +#include "tom/migrator.hpp" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "tom/exceptions/invalidtemplateargumenterror.hpp" +#include "tom/migration.hpp" +#include "tom/migrationrepository.hpp" + +using Orm::DatabaseConnection; + +using Orm::Constants::DESC; + +using QueryUtils = Orm::Utils::Query; +using TypeUtils = Orm::Utils::Type; + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + +/* public */ + +Migrator::Migrator( + std::shared_ptr &&repository, + std::shared_ptr &&resolver, + const std::vector> &migrations, + const QCommandLineParser &parser +) + : Concerns::InteractsWithIO(parser) + , m_repository(std::move(repository)) + , m_resolver(std::move(resolver)) + , m_migrations(migrations) +{ + /* Initialize these containers in the constructor as every command that uses + the Migrator also needs a migrations map or names list. */ + createMigrationNamesMap(); +} + +/* Main migrate operations */ + +std::vector> Migrator::run(const MigrateOptions options) const +{ + auto migrations = pendingMigrations(m_repository->getRanSimple()); + + /* First we will just make sure that there are any migrations to run. If there + aren't, we will just make a note of it to the developer so they're aware + that all of the migrations have been run against this database system. */ + if (migrations.empty()) { + info(QLatin1String("Nothing to migrate.")); + + return migrations; + } + + /* Next, we will get the next batch number for the migrations so we can insert + correct batch number in the database migrations repository when we store + each migration's execution. We will also extract a few of the options. */ + auto batch = m_repository->getNextBatchNumber(); + + const auto &[pretend, step, _] = options; + + /* Once we have the vector of migrations, we will spin through them and run the + migrations "up" so the changes are made to the databases. We'll then log + that the migration was run so we don't repeat it next time we execute. */ + for (const auto &migration : migrations) { + runUp(*migration, batch, pretend); + + if (step) + ++batch; + } + + return migrations; +} + +std::vector +Migrator::rollback(const MigrateOptions options) const +{ + /* We want to pull in the last batch of migrations that ran on the previous + migration operation. We'll then reverse those migrations and run each + of them "down" to reverse the last migration "operation" which ran. */ + return rollbackMigrations(getMigrationsForRollback(options), options.pretend); +} + +std::vector Migrator::reset(const bool pretend) const +{ + /* Reverse all the ran migrations list to reset this database. This will allow us + to get the database back into its "empty" state, ready for the migrations. */ + return rollbackMigrations(getMigrationsForRollback(m_repository->getRan(DESC)), + pretend); +} + +/* Database connection related */ + +int Migrator::usingConnection(QString &&name, const bool debugSql, + std::function &&callback) +{ + auto previousConnection = m_resolver->getDefaultConnection(); + /* Default connection can also be "" empty string, eg. auto tests are using empty + string as the default connection. */ + auto previousDebugSql = getConnectionDebugSql(previousConnection); + + setConnection(std::move(name), debugSql); + + auto exitCode = std::invoke(std::move(callback)); + + setConnection(std::move(previousConnection), std::move(previousDebugSql)); + + return exitCode; +} + +/* Proxies to MigrationRepository */ + +bool Migrator::repositoryExists() const +{ + return m_repository->repositoryExists(); +} + +bool Migrator::hasRunAnyMigrations() const +{ + return repositoryExists() && !m_repository->getRanSimple().isEmpty(); +} + +/* Getters / Setters */ + +void Migrator::setConnection(QString &&name, std::optional &&debugSql) +{ + // It indicates "" empty string for the default connection, eg. in auto tests + if (!name.isEmpty()) + m_resolver->setDefaultConnection(name); + + m_repository->setConnection(name, std::move(debugSql)); + + m_connection = std::move(name); +} + +/* protected */ + +/* Database connection related */ + +DatabaseConnection &Migrator::resolveConnection(const QString &name) const +{ + return m_resolver->connection(name.isEmpty() ? m_connection : name); +} + +std::optional Migrator::getConnectionDebugSql(const QString &name) const +{ + return name.isEmpty() ? std::nullopt + : std::make_optional(m_resolver->connection(name).debugSql()); +} + +/* Migration instances lists and hashes */ + +void Migrator::createMigrationNamesMap() +{ + QString previousMigrationName; + + for (const auto &migration : m_migrations) { + // To avoid expression with side effects in the typeid () + const auto &migrationRef = *migration; + + // mid(1) tp remove the '_' at beginning + auto migrationName = TypeUtils::classPureBasename(migrationRef, false).mid(1); + + // Verify alphabetical sorting + throwIfMigrationsNotSorted(previousMigrationName, migrationName); + previousMigrationName = migrationName; + + m_migrationNamesMap.emplace(std::type_index(typeid (migrationRef)), + migrationName); + + m_migrationInstancesMap.emplace(migrationName, migration); + + m_migrationNames.emplace(std::move(migrationName)); + } +} + +QString Migrator::getMigrationName(const Migration &migration) const +{ + // To avoid expression with side effects in the typeid () + const auto &migrationRef = migration; + const auto &migrationId = typeid (migrationRef); + + Q_ASSERT(m_migrationNamesMap.contains(migrationId)); + + return m_migrationNamesMap.at(migrationId); +} + +/* Migrate */ + +std::vector> +Migrator::pendingMigrations(const QVector &ran) const +{ + return m_migrations + | ranges::views::remove_if([this, &ran](const auto &migration) + { + return ran.contains(getMigrationName(*migration)); + }) + | ranges::to>>(); +} + +void Migrator::runUp(const Migration &migration, const int batch, + const bool pretend) const +{ + if (pretend) { + pretendToRun(migration, MigrateMethod::Up); + return; + } + + auto migrationName = getMigrationName(migration); + + comment(QLatin1String("Migrating: "), false).note(migrationName); + + QElapsedTimer timer; + timer.start(); + + runMigration(migration, MigrateMethod::Up); + + const auto elapsedTime = timer.elapsed(); + + /* Once we have run a migrations class, we will log that it was run in this + repository so that we don't try to run it next time we do a migration + in the application. A migration repository keeps the migrate order. */ + m_repository->log(migrationName, batch); + + info(QLatin1String("Migrated: "), false); + note(QStringLiteral("%1 (%2ms)").arg(std::move(migrationName)).arg(elapsedTime)); +} + +/* Rollback */ + +std::vector +Migrator::getMigrationsForRollback(const MigrateOptions options) const +{ + auto migrationsDb = options.stepValue > 0 + ? m_repository->getMigrations(options.stepValue) + : m_repository->getLast(); + + return m_migrations + | ranges::views::reverse + | ranges::views::filter([this, &migrationsDb](const auto &migration) + { + return ranges::contains(migrationsDb, getMigrationName(*migration), + [](const auto &m) { return m.migration; }); + }) + | ranges::views::transform([this, &migrationsDb](const auto &migration) + -> RollbackItem + { + // Can not happen that it doesn't find, checked in previous lambda by 'contains' + auto &&[id, migrationName, _] = + *std::ranges::find(migrationsDb, getMigrationName(*migration), + [](const auto &m) { return m.migration; }); + + return {std::move(id), std::move(migrationName), migration}; + }) + | ranges::to>(); +} + +std::vector +Migrator::getMigrationsForRollback(std::vector &&ran) const +{ + return ranges::views::move(ran) + | ranges::views::transform([this](auto &&migrationItem) -> RollbackItem + { + auto &&[id, migrationName, _] = migrationItem; + + auto migration = m_migrationInstancesMap.at(migrationName); + + return {std::move(id), std::move(migrationName), std::move(migration)}; + }) + | ranges::to>(); +} + +std::vector +Migrator::rollbackMigrations(std::vector &&migrations, + const bool pretend) const +{ + if (migrations.empty()) { + info(QLatin1String("Nothing to rollback")); + + return std::move(migrations); + } + + for (const auto &migration : migrations) + runDown(migration, pretend); + + return std::move(migrations); +} + +void Migrator::runDown(const RollbackItem &migrationToRollback, const bool pretend) const +{ + const auto &[id, migrationName, migration] = migrationToRollback; + + if (pretend) { + pretendToRun(*migration, MigrateMethod::Down); + return; + } + + comment(QLatin1String("Rolling back: "), false).note(migrationName); + + QElapsedTimer timer; + timer.start(); + + runMigration(*migration, MigrateMethod::Down); + + const auto elapsedTime = timer.elapsed(); + + /* Once we have successfully run the migration "down" we will remove it from + the migration repository so it will be considered to have not been run + by the application then will be able to fire by any later operation. */ + m_repository->deleteMigration(id); + + info(QLatin1String("Rolled back: "), false); + note(QStringLiteral("%1 (%2ms)").arg(migrationName).arg(elapsedTime)); +} + +/* Pretend */ + +void Migrator::pretendToRun(const Migration &migration, const MigrateMethod method) const +{ + for (auto &&query : getQueries(migration, method)) { + info(QStringLiteral("%1:").arg(getMigrationName(migration))); + + note(QueryUtils::parseExecutedQueryForPretend(query.query, + query.boundValues)); + } +} + +QVector +Migrator::getQueries(const Migration &migration, const MigrateMethod method) const +{ + /* Now that we have the connections we can resolve it and pretend to run the + queries against the database returning the array of raw SQL statements + that would get fired against the database system for this migration. */ + return resolveConnection(migration.connection) + .pretend([this, &migration, method]() + { + migrateByMethod(migration, method); + }); +} + +/* Migrate up/down common */ + +void Migrator::runMigration(const Migration &migration, const MigrateMethod method) const +{ + auto &connection = resolveConnection(migration.connection); + + // Invoke migration in the transaction if a database driver supports it + const auto withinTransaction = + connection.getSchemaGrammar().supportsSchemaTransactions() && + migration.withinTransaction; + + // Without transaction + if (!withinTransaction) { + migrateByMethod(migration, method); + return; + } + + // Transactional migration + connection.beginTransaction(); + + try { + migrateByMethod(migration, method); + + } catch (const std::exception &/*unused*/) { + + connection.rollBack(); + // Re-throw + throw; + } + + connection.commit(); +} + +void Migrator::migrateByMethod(const Migration &migration, + const MigrateMethod method) const +{ + switch (method) { + case MigrateMethod::Up: + migration.up(); + return; + + case MigrateMethod::Down: + migration.down(); + return; + } + + Q_UNREACHABLE(); +} + +void Migrator::throwIfMigrationsNotSorted(const QString &previousMigrationName, + const QString &migrationName) const +{ + if (previousMigrationName < migrationName) + return; + + throw Exceptions::InvalidTemplateArgumentError( + QLatin1String( + "The template arguments passed to the TomApplication::migrations() " + "must always be sorted alphabetically.")); +} + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/src/tom/terminal.cpp b/tom/src/tom/terminal.cpp new file mode 100644 index 000000000..0a82b0125 --- /dev/null +++ b/tom/src/tom/terminal.cpp @@ -0,0 +1,259 @@ +#include "tom/terminal.hpp" + +#ifdef _WIN32 +# include + +# include +# include +#elif defined(__linux__) +# include +#endif + +#include + +#include "tom/exceptions/invalidargumenterror.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Tom +{ + +/* public */ + +// I can tell that ansi logic detection in this class is a real porn 😎 + +void Terminal::initialize() +{ +#ifdef _WIN32 + enableUtf8ConsoleEncoding(); +#endif + +#ifdef __MINGW32__ + enableVt100Support(); +#endif +} + +bool Terminal::hasColorSupport(std::ostream &cout) const +{ + auto *const coutPointer = std::addressof(cout); + + // Return from the cache, compute only once + if (m_isAnsiOutput.contains(coutPointer)) + return m_isAnsiOutput[coutPointer]; + + // Map c++ stream to the c stream, needed by the isatty() + FILE *stream = nullptr; + + if (coutPointer == std::addressof(std::cout)) + stream = stdout; + else if (coutPointer == std::addressof(std::cerr)) + stream = stderr; + else + throw Exceptions::InvalidArgumentError( + QLatin1String("Unsupported stream type passed in %1().") + .arg(__tiny_func__)); + + // Autodetect + const auto isAnsi = hasColorSupportInternal(cout, stream); + + // Cache the result + m_isAnsiOutput.emplace(coutPointer, isAnsi); + + return isAnsi; +} + +bool Terminal::hasWColorSupport(std::wostream &wcout) const +{ + auto *const wcoutPointer = std::addressof(wcout); + + // Return from the cache, compute only once + if (m_isAnsiWOutput.contains(wcoutPointer)) + return m_isAnsiWOutput[wcoutPointer]; + + // Map c++ stream to the c stream, needed by the isatty() + FILE *stream = nullptr; + + if (wcoutPointer == std::addressof(std::wcout)) + stream = stdout; + else if (wcoutPointer == std::addressof(std::wcerr)) + stream = stderr; + else + throw Exceptions::InvalidArgumentError( + QLatin1String("Unsupported stream type passed in %1().") + .arg(__tiny_func__)); + + // Autodetect + const auto isAnsi = hasColorSupportInternal(wcout, stream); + + // Cache the result + m_isAnsiWOutput.emplace(wcoutPointer, isAnsi); + + return isAnsi; +} + +bool Terminal::isatty(FILE *stream) const +{ +#ifdef _WIN32 + return ::_isatty(::_fileno(stream)) != 0; +#else + return ::isatty(::fileno(stream)) != 0; +#endif +} + +int Terminal::width() +{ + if (const auto widthRaw = qEnvironmentVariable("COLUMNS"); + !widthRaw.isEmpty() + ) { + auto ok = false; + const auto width = widthRaw.toInt(&ok); + + if (ok) + return m_lastWidth = width; + } + + if (auto [width, _] = terminalSize(); width != -1) + return m_lastWidth = width; + + return m_lastWidth; +} + +int Terminal::height() +{ + if (const auto heightRaw = qEnvironmentVariable("LINES"); + !heightRaw.isEmpty() + ) { + auto ok = false; + const auto height = heightRaw.toInt(&ok); + + if (ok) + return m_lastHeight = height; + } + + if (auto [_, height] = terminalSize(); height != -1) + return m_lastHeight = height; + + return m_lastHeight; +} + +Terminal::TerminalSize Terminal::terminalSize() const +{ + int width = -1; + int height = -1; + +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + + width = static_cast(csbi.srWindow.Right - csbi.srWindow.Left) + 1; + height = static_cast(csbi.srWindow.Bottom - csbi.srWindow.Top) + 1; +#elif defined(__linux__) + // CUR tom, finish linux silverqx + struct winsize w; + ioctl(fileno(stdout), TIOCGWINSZ, &w); + + width = static_cast(w.ws_col); + height = static_cast(w.ws_row); +#endif + + return {width, height}; +} + +/* private */ + +#ifdef _WIN32 +namespace +{ + /*! Get Windows output handle by passed c++ output stream. */ + HANDLE getStdHandleByCppStream(std::ostream &cout = std::cout) + { + HANDLE handle = nullptr; + + if (std::addressof(cout) == std::addressof(std::cout)) + handle = GetStdHandle(STD_OUTPUT_HANDLE); + else if (std::addressof(cout) == std::addressof(std::cerr)) + handle = GetStdHandle(STD_ERROR_HANDLE); + else + throw Exceptions::InvalidArgumentError( + QLatin1String("Unsupported stream type passed in %1().") + .arg(__tiny_func__)); + + return handle; + } + + /*! Get Windows output handle by passed c++ output stream, wide version. */ + HANDLE getStdHandleByCppStream(std::wostream &wcout = std::wcout) + { + HANDLE handle = nullptr; + + if (std::addressof(wcout) == std::addressof(std::wcout)) + handle = GetStdHandle(STD_OUTPUT_HANDLE); + else if (std::addressof(wcout) == std::addressof(std::wcerr)) + handle = GetStdHandle(STD_ERROR_HANDLE); + else + throw Exceptions::InvalidArgumentError( + QLatin1String("Unsupported stream type passed in %1().") + .arg(__tiny_func__)); + + return handle; + } +} // namespace + +bool Terminal::hasVt100Support(std::ostream &cout) const +{ + DWORD mode = 0; + + if (GetConsoleMode(getStdHandleByCppStream(cout), &mode) == 0) + return false; + + return (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == + ENABLE_VIRTUAL_TERMINAL_PROCESSING; +} + +bool Terminal::hasVt100Support(std::wostream &wcout) const +{ + DWORD mode = 0; + + if (GetConsoleMode(getStdHandleByCppStream(wcout), &mode) == 0) + return false; + + return (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == + ENABLE_VIRTUAL_TERMINAL_PROCESSING; +} + +/* Terminal initialization */ + +void Terminal::enableUtf8ConsoleEncoding() +{ + // Set it here so the user doesn't have to deal with this + SetConsoleOutputCP(CP_UTF8); + + /* UTF-8 encoding is corrupted for narrow input functions, needed to use wcin/wstring + for an input, input will be in the unicode encoding then needed to translate + unicode to utf8, eg. by QString::fromStdWString(), WideCharToMultiByte(), or + std::codecvt(). It also works with msys2 ucrt64 gcc/clang. */ + SetConsoleCP(CP_UTF8); + _setmode(_fileno(stdin), _O_WTEXT); +} +#endif + +#ifdef __MINGW32__ +void Terminal::enableVt100Support() +{ + /* The vt100 is disabled by default on MSYS2 so have to be explicitly enabled: + https://github.com/msys2/msys2-runtime/issues/91 */ + DWORD mode = 0; + auto *const stdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleMode(stdOutHandle, &mode); + SetConsoleMode(stdOutHandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + mode = 0; + auto *const stdErrHandle = GetStdHandle(STD_ERROR_HANDLE); + GetConsoleMode(stdErrHandle, &mode); + SetConsoleMode(stdErrHandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); +} +#endif + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE diff --git a/vcpkg.json b/vcpkg.json index af271d2f2..73a9db0b8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -8,7 +8,8 @@ "maintainers": "Silver Zachara ", "supports": "!(uwp | arm | android | emscripten)", "dependencies": [ - "range-v3" + "range-v3", + "tabulate" ], "features": { "mysqlping": {