From d03d8fc9720390f5a3c24f6d3de9d263b2861fd3 Mon Sep 17 00:00:00 2001 From: silverqx Date: Sun, 27 Mar 2022 16:12:44 +0200 Subject: [PATCH 01/23] tom initial commit - finished qmake build --- TinyOrm.pro | 7 ++- include/orm/tiny/model.hpp | 3 ++ qmake/features/disable_tom.prf | 2 + tom/include/include.pri | 5 ++ tom/include/pch.h | 45 ++++++++++++++++ tom/include/pch.pri | 8 +++ tom/include/tom/config.hpp | 17 ++++++ tom/include/tom/version.hpp | 47 +++++++++++++++++ tom/resources/tom.exe.manifest | 39 ++++++++++++++ tom/resources/tom.rc.in | 55 ++++++++++++++++++++ tom/src/src.pri | 2 + tom/src/src.pro | 95 ++++++++++++++++++++++++++++++++++ tom/src/tom/main.cpp | 68 ++++++++++++++++++++++++ tom/tom.pro | 9 ++++ 14 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 qmake/features/disable_tom.prf create mode 100644 tom/include/include.pri create mode 100644 tom/include/pch.h create mode 100644 tom/include/pch.pri create mode 100644 tom/include/tom/config.hpp create mode 100644 tom/include/tom/version.hpp create mode 100644 tom/resources/tom.exe.manifest create mode 100644 tom/resources/tom.rc.in create mode 100644 tom/src/src.pri create mode 100644 tom/src/src.pro create mode 100644 tom/src/tom/main.cpp create mode 100644 tom/tom.pro diff --git a/TinyOrm.pro b/TinyOrm.pro index 80cd39659..67d2d3a3c 100644 --- a/TinyOrm.pro +++ b/TinyOrm.pro @@ -1,6 +1,11 @@ TEMPLATE = subdirs -SUBDIRS += src +SUBDIRS = src + +!disable_tom { + SUBDIRS += tom + tom.depends = src +} # Can be enabled by CONFIG += build_tests when the qmake.exe for the project is called build_tests { diff --git a/include/orm/tiny/model.hpp b/include/orm/tiny/model.hpp index aa4f452da..cd7997f73 100644 --- a/include/orm/tiny/model.hpp +++ b/include/orm/tiny/model.hpp @@ -1327,3 +1327,6 @@ 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, check tom src.pro will solving vcpkg builds again silverqx +// CUR tom docs, disable_tom and TINYORM_DISABLE_TOM to build.mdx silverqx 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/tom/include/include.pri b/tom/include/include.pri new file mode 100644 index 000000000..f3f8ad892 --- /dev/null +++ b/tom/include/include.pri @@ -0,0 +1,5 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/tom/config.hpp \ + $$PWD/tom/version.hpp \ diff --git a/tom/include/pch.h b/tom/include/pch.h new file mode 100644 index 000000000..ac3d59ed6 --- /dev/null +++ b/tom/include/pch.h @@ -0,0 +1,45 @@ +/* This file can't be included in the project, it's for a precompiled header. */ + +/* Add C includes here */ + +#if defined __cplusplus +/* Add C++ includes here */ +//#include +//#include +//#include +//#include +#include +//#include +//#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +//#include +#include +#include +#include +#include +#include +#include +#endif diff --git a/tom/include/pch.pri b/tom/include/pch.pri new file mode 100644 index 000000000..62b027493 --- /dev/null +++ b/tom/include/pch.pri @@ -0,0 +1,8 @@ +# Use Precompiled headers (PCH) +# --- + +PRECOMPILED_HEADER = $$quote($$PWD/pch.h) +HEADERS += $$PRECOMPILED_HEADER + +precompile_header: \ + DEFINES *= USING_PCH diff --git a/tom/include/tom/config.hpp b/tom/include/tom/config.hpp new file mode 100644 index 000000000..a0a46d2ef --- /dev/null +++ b/tom/include/tom/config.hpp @@ -0,0 +1,17 @@ +#pragma once +#ifndef TOM_CONFIG_HPP +#define TOM_CONFIG_HPP + +#include "orm/macros/systemheader.hpp" +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/version.hpp b/tom/include/tom/version.hpp new file mode 100644 index 000000000..de6ddaa94 --- /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 "orm/macros/systemheader.hpp" +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..74930faa0 --- /dev/null +++ b/tom/resources/tom.rc.in @@ -0,0 +1,55 @@ +//#pragma code_page(65001) // UTF-8 + +//IDI_ICON1 ICON "icons/@tom_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 "@tom_target@$.manifest" +#endif diff --git a/tom/src/src.pri b/tom/src/src.pri new file mode 100644 index 000000000..d0023c38d --- /dev/null +++ b/tom/src/src.pri @@ -0,0 +1,2 @@ +SOURCES += \ + $$PWD/tom/main.cpp \ diff --git a/tom/src/src.pro b/tom/src/src.pro new file mode 100644 index 000000000..66a975bad --- /dev/null +++ b/tom/src/src.pro @@ -0,0 +1,95 @@ +QT *= core sql +QT -= gui + +TEMPLATE = app +TARGET = tom + +# TinyTom application specific configuration +# --- + +CONFIG *= console + +# Common Configuration +# --- + +include($$TINYORM_SOURCE_TREE/qmake/common.pri) + +# TinyTom defines +# --- + +DEFINES += PROJECT_TINYTOM + +# Release build +CONFIG(release, debug|release): DEFINES += TINYTOM_NO_DEBUG +# Debug build +CONFIG(debug, debug|release): DEFINES *= TINYTOM_DEBUG + +# Enable code needed by tests +#build_tests: \ +# DEFINES *= TINYTOM_TESTS_CODE + +# TinyTom library header and source files +# --- + +# tiny_version_numbers() depends on HEADERS (version.hpp) +include(../include/include.pri) +include(src.pri) + +# 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($$PWD/../include/) +# Find Windows manifest +mingw: tinyRcIncludepath += $$quote($$PWD/../resources/) + +load(tiny_resource_and_manifest) +tiny_resource_and_manifest($$tinyRcIncludepath, ../resources) + +# Use Precompiled headers (PCH) +# --- + +include(../include/pch.pri) + +# 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 +} + +# Configure TinyORM library +# --- + +include($$TINYORM_SOURCE_TREE/qmake/TinyOrm.pri) + +# User Configuration +# --- + +exists(../conf.pri): \ + include(../conf.pri) + +#else:is_vcpkg_build: \ +# include(../qmake/vcpkgconf.pri) + +else: \ + error( "'conf.pri' for 'tom' project does not exist. See an example configuration\ + in 'tom/conf.pri.example' or call 'vcpkg install' in the project's root." ) + +# Link against TinyORM library +# --- + +INCLUDEPATH += $$quote($$TINYORM_SOURCE_TREE/include/) + +LIBS += $$quote(-L$$TINYORM_BUILD_TREE/src$${TINY_RELEASE_TYPE}/) +LIBS += -lTinyOrm diff --git a/tom/src/tom/main.cpp b/tom/src/tom/main.cpp new file mode 100644 index 000000000..e0e4dfece --- /dev/null +++ b/tom/src/tom/main.cpp @@ -0,0 +1,68 @@ +#include + +#if defined(_WIN32) +#include +#endif + +#include + +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_; + +int main(int /*unused*/, char */*unused*/[]) +//int main(int argc, char *argv[]) +{ +// QCoreApplication app(argc, argv); +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); +// SetConsoleOutputCP(1250); +#endif + + QCoreApplication::setOrganizationName("TinyORM"); + QCoreApplication::setOrganizationDomain("tinyorm.org"); + QCoreApplication::setApplicationName("tom"); + + qDebug() << "tom"; + + auto dm = Orm::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"))}, + // CUR add timezone names to the MySQL server and test them silverqx + {timezone_, SYSTEM}, + {prefix_, ""}, + {prefix_indexes, true}, + {strict_, true}, + {isolation_level, QStringLiteral("REPEATABLE READ")}, + {engine_, InnoDB}, + {options_, QVariantHash()}, + }); + +// return QCoreApplication::exec(); + return 0; +} diff --git a/tom/tom.pro b/tom/tom.pro new file mode 100644 index 000000000..ef431d5d9 --- /dev/null +++ b/tom/tom.pro @@ -0,0 +1,9 @@ +TEMPLATE = subdirs + +SUBDIRS = src + +# Can be enabled by CONFIG += build_tests when the qmake.exe for the project is called +#build_tests { +# SUBDIRS += tests +# tests.depends = src +#} From 16575bef58485ae8a141c23a33fd3bc2ecc379b4 Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:12:57 +0200 Subject: [PATCH 02/23] docs added pragma once --- docs/hello-world.mdx | 2 +- docs/tinyorm-relationships.mdx | 10 ++++++++++ docs/tinyorm.mdx | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/hello-world.mdx b/docs/hello-world.mdx index 4ec6497fc..06864fe6c 100644 --- a/docs/hello-world.mdx +++ b/docs/hello-world.mdx @@ -156,7 +156,7 @@ And paste the following code. #include #ifdef _WIN32 -#include +# include #endif #include diff --git a/docs/tinyorm-relationships.mdx b/docs/tinyorm-relationships.mdx index c8da73ecc..138f92ef5 100644 --- a/docs/tinyorm-relationships.mdx +++ b/docs/tinyorm-relationships.mdx @@ -48,6 +48,7 @@ But, before diving too deep into using relationships, let's learn how to define Before you start defining relationship methods, you have to declare a model class, let's examine following model class with a "one" type relation: + #pragma once #ifndef USER_HPP #define USER_HPP @@ -94,6 +95,7 @@ You may omit the `friend Model` declaration and define all the private data and A one-to-one relationship is a very basic type of database relationship. For example, a `User` model might be associated with one `Phone` model. To define this relationship, we will place a `phone` method on the `User` model. The `phone` method should call the `hasOne` method and return its result. The `hasOne` method is available to your model via the model's `Orm::Tiny::Model` base class: + #pragma once #ifndef USER_HPP #define USER_HPP @@ -142,6 +144,7 @@ Additionally, TinyORM assumes that the foreign key should have a value matching So, we can access the `Phone` model from our `User` model. Next, let's define a relationship on the `Phone` model that will let us access the user that owns the phone. We can define the inverse of a `hasOne` relationship using the `belongsTo` method: + #pragma once #ifndef PHONE_HPP #define PHONE_HPP @@ -213,6 +216,7 @@ The relation name will be guessed from the type-id of the `Related` template par A one-to-many relationship is used to define relationships where a single model is the parent to one or more child models. For example, a blog post may have an infinite number of comments. Like all other TinyORM relationships, one-to-many relationships are defined by defining a `hasMany` method on your TinyORM model: + #pragma once #ifndef POST_HPP #define POST_HPP @@ -273,6 +277,7 @@ Like the `hasOne` method, you may also override the foreign and local keys by pa Now that we can access all of a post's comments, let's define a relationship to allow a comment to access its parent post. To define the inverse of a `hasMany` relationship, define a relationship method on the child model which calls the `belongsTo` method: + #pragma once #ifndef COMMENT_HPP #define COMMENT_HPP @@ -406,6 +411,7 @@ Remember, since a role can belong to many users, we cannot simply place a `user_ Many-to-many relationships are defined by writing a method that returns the result of the `belongsToMany` method. The `belongsToMany` method is provided by the `Orm::Tiny::Model` base class that is used by all of your application's TinyORM models. For example, let's define a `roles` method on our `User` model. The first argument passed to this method is the name of the related model class: + #pragma once #ifndef USER_HPP #define USER_HPP @@ -471,6 +477,7 @@ The relation name is used during [Touching Parent Timestamps](#touching-parent-t To define the "inverse" of a many-to-many relationship, you should define a method on the related model which also returns the result of the `belongsToMany` method. To complete our user / role example, let's define the `users` method on the `Role` model: + #pragma once #ifndef ROLE_HPP #define ROLE_HPP @@ -582,6 +589,7 @@ If you would like to define a custom model to represent the intermediate table o Custom many-to-many pivot models should extend the `Orm::Tiny::Relations::BasePivot` class. For example, we may define a `User` model which uses a custom `RoleUser` pivot model: + #pragma once #ifndef USER_HPP #define USER_HPP @@ -618,6 +626,7 @@ Custom many-to-many pivot models should extend the `Orm::Tiny::Relations::BasePi When defining the `RoleUser` model, you should extend the `Orm::Tiny::Relations::BasePivot` class: + #pragma once #ifndef ROLEUSER_HPP #define ROLEUSER_HPP @@ -637,6 +646,7 @@ When defining the `RoleUser` model, you should extend the `Orm::Tiny::Relations: You have to pass a custom pivot type to the `AllRelations` template parameter pack on `Model` so that the `Model` knows how to generate a `std::variant`, which holds all the relations and also you have to add a new mapping from the relation name to the custom pivot model type-id, this is described in more detail in the [Common Rules](#common-rules): + #pragma once #ifndef ROLE_HPP #define ROLE_HPP diff --git a/docs/tinyorm.mdx b/docs/tinyorm.mdx index b8d034953..fb9251b0f 100644 --- a/docs/tinyorm.mdx +++ b/docs/tinyorm.mdx @@ -37,6 +37,7 @@ Before getting started, be sure to configure a database connection in your appli Let's examine a basic model class and discuss some of TinyORM's key conventions: + #pragma once #ifndef FLIGHT_HPP #define FLIGHT_HPP From b6f9b7899a84ba47734737f8bcec4578d89bb9aa Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:18:11 +0200 Subject: [PATCH 03/23] add indent in #ifdef --- include/orm/tiny/concerns/hasrelationships.hpp | 4 ++-- include/orm/tiny/model.hpp | 2 +- include/orm/tiny/types/syncchanges.hpp | 2 +- include/orm/utils/type.hpp | 2 +- include/orm/version.hpp | 2 +- src/orm/concerns/logsqueries.cpp | 4 ++-- src/orm/databaseconnection.cpp | 2 +- src/orm/mysqlconnection.cpp | 2 +- src/orm/utils/thread.cpp | 4 ++-- src/orm/utils/type.cpp | 4 ++-- tests/auto/unit/orm/version/tst_version.cpp | 2 +- tests/models/models/torrent.hpp | 2 +- tom/include/tom/version.hpp | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/include/orm/tiny/concerns/hasrelationships.hpp b/include/orm/tiny/concerns/hasrelationships.hpp index efd3a88b8..cde930875 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 diff --git a/include/orm/tiny/model.hpp b/include/orm/tiny/model.hpp index cd7997f73..6668d7f0d 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 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/utils/type.hpp b/include/orm/utils/type.hpp index 3d482573b..26ce3053f 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" 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/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..48caea086 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" 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/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..ba69e5886 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; diff --git a/tests/auto/unit/orm/version/tst_version.cpp b/tests/auto/unit/orm/version/tst_version.cpp index e5524fa74..671afb4ab 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" 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/tom/include/tom/version.hpp b/tom/include/tom/version.hpp index de6ddaa94..0c17e4724 100644 --- a/tom/include/tom/version.hpp +++ b/tom/include/tom/version.hpp @@ -4,7 +4,7 @@ // Excluded for the Resource compiler #ifndef RC_INVOKED -#include "orm/macros/systemheader.hpp" +# include TINY_SYSTEM_HEADER #endif From d5f96324703a5bd42f5a74c8cedcb5c66b9199dc Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:22:39 +0200 Subject: [PATCH 04/23] enhanced exception classes - normalized comments - added a new std::string ctor - getters noexcept --- include/orm/exceptions/domainerror.hpp | 4 +--- include/orm/exceptions/invalidargumenterror.hpp | 4 +--- include/orm/exceptions/invalidformaterror.hpp | 2 +- include/orm/exceptions/invalidtemplateargumenterror.hpp | 2 +- include/orm/exceptions/logicerror.hpp | 9 +++++---- include/orm/exceptions/queryerror.hpp | 2 +- include/orm/exceptions/runtimeerror.hpp | 9 +++++---- include/orm/exceptions/sqlerror.hpp | 9 +++++++-- include/orm/exceptions/sqltransactionerror.hpp | 2 +- src/orm/exceptions/logicerror.cpp | 4 ++++ src/orm/exceptions/runtimeerror.cpp | 4 ++++ src/orm/exceptions/sqlerror.cpp | 5 ----- 12 files changed, 31 insertions(+), 25 deletions(-) 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/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); From 5ab1e7726a1bace6e41d805ff85dd92eab6b7ae2 Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:25:26 +0200 Subject: [PATCH 05/23] small changes --- include/orm/databaseconnection.hpp | 7 +------ include/orm/macros/commonnamespace.hpp | 1 + src/orm/databaseconnection.cpp | 10 +++++++++- 3 files changed, 11 insertions(+), 7 deletions(-) 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/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/src/orm/databaseconnection.cpp b/src/orm/databaseconnection.cpp index 48caea086..29724602d 100644 --- a/src/orm/databaseconnection.cpp +++ b/src/orm/databaseconnection.cpp @@ -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}, From 5cd54ebbef2e377f96ae3a1eb026b3cb1ee6021c Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:32:42 +0200 Subject: [PATCH 06/23] converted tiny utils to library classes - added Type::isTrue() - added studly() and splitStringByWidth() to StringUtils - used using XxUtils for all Tiny library classes everywhere --- include/orm/tiny/concerns/hasattributes.hpp | 19 +- .../orm/tiny/concerns/hasrelationships.hpp | 1 - include/orm/tiny/model.hpp | 22 ++- include/orm/tiny/relations/basepivot.hpp | 7 +- include/orm/tiny/relations/belongstomany.hpp | 14 +- include/orm/tiny/relations/hasoneormany.hpp | 8 +- include/orm/tiny/tinybuilder.hpp | 13 +- include/orm/tiny/tinybuilderproxies.hpp | 15 +- include/orm/tiny/utils/attribute.hpp | 57 +++--- include/orm/tiny/utils/string.hpp | 43 ++++- include/orm/utils/type.hpp | 3 + src/orm/tiny/utils/attribute.cpp | 21 +- src/orm/tiny/utils/string.cpp | 182 ++++++++++++++++-- src/orm/utils/type.cpp | 5 + 14 files changed, 312 insertions(+), 98 deletions(-) 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 cde930875..3569f95e7 100644 --- a/include/orm/tiny/concerns/hasrelationships.hpp +++ b/include/orm/tiny/concerns/hasrelationships.hpp @@ -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 6668d7f0d..ee46e1e1b 100644 --- a/include/orm/tiny/model.hpp +++ b/include/orm/tiny/model.hpp @@ -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(); @@ -1328,5 +1330,11 @@ TINYORM_END_COMMON_NAMESPACE // 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, check tom src.pro will solving vcpkg builds again silverqx -// CUR tom docs, disable_tom and TINYORM_DISABLE_TOM to build.mdx 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 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/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 26ce3053f..703e07b2d 100644 --- a/include/orm/utils/type.hpp +++ b/include/orm/utils/type.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/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..5f1cb94a7 100644 --- a/src/orm/tiny/utils/string.cpp +++ b/src/orm/tiny/utils/string.cpp @@ -1,20 +1,43 @@ #include "orm/tiny/utils/string.hpp" -#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); +} // 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 +56,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); +} // 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 +120,109 @@ 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 +/*! Split a string by the given width (not in the middle of a word). */ +std::vector String::splitStringByWidth(const QString &string, const int width) +{ + const auto stringSize = string.size(); + + // Nothing to split + if (stringSize <= width) + return {string}; + + QString::size_type from = 0; + + std::vector lines; + + while (true) { + /* Section - find split position */ + auto searchFrom = from + width - 1; + + // Hit the end - the last text block - !(searchFrom < stringSize) + const auto isEnd = searchFrom >= stringSize - 1; + + // Returns pos == -1 if not found + auto pos = string.lastIndexOf(SPACE, isEnd ? -1 : searchFrom); + + /* Section - compute how much chars to copy */ + QString::size_type copySize = -1; + + // The last text block, -1 for copy the rest + if (isEnd) + copySize = -1; + + // If pos == -1 (no space found) and not at end then copy a whole width block + else if (pos == -1 && !isEnd) + copySize = width; + + // Copy to the found space char + else + copySize = pos - from; + + /* Section - done, copy a text */ + lines.emplace_back(string.mid(from, copySize)); + + // Hit the end - the last text block - !(searchFrom < stringSize) + if (isEnd) + break; + + /* Section - prepare 'from' for the next loop */ + /* Start after the whole width block (if pos == -1 then pos + 1 is not + correct here). */ + if (pos == -1) { + from += width; + + // When whole block was copied, the next char can or can not be a space + if (string.at(from) == SPACE) + ++from; + } + // Start from a last found space + /* +1 means - don't copy a space at beginning (skip space at beginning), + is guaranteed that the first char will be a space. */ + else + from = pos + 1; + } + + 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/type.cpp b/src/orm/utils/type.cpp index ba69e5886..b73fa5957 100644 --- a/src/orm/utils/type.cpp +++ b/src/orm/utils/type.cpp @@ -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) { From 80450d68b6d7079e86973bced9a78c0ec3b3dfb8 Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:34:26 +0200 Subject: [PATCH 07/23] moved TINYORM_TESTS_CODE to TinyOrm.pri --- tests/qmake/TinyOrm.pri | 3 +++ tests/qmake/common.pri | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/qmake/TinyOrm.pri b/tests/qmake/TinyOrm.pri index 4a7985e4f..6bebbff25 100644 --- a/tests/qmake/TinyOrm.pri +++ b/tests/qmake/TinyOrm.pri @@ -5,6 +5,9 @@ CONFIG(shared, dll|shared|static|staticlib) | \ CONFIG(dll, dll|shared|static|staticlib): \ DEFINES += TINYORM_LINKING_SHARED +# Enable code needed by tests, eg connection overriding in the Model +DEFINES *= TINYORM_TESTS_CODE + # TinyORM library headers include path # --- diff --git a/tests/qmake/common.pri b/tests/qmake/common.pri index 177bb9dc3..065b66759 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) # --- From 8db628b9650443b06a1526968443286a81e539a2 Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:35:07 +0200 Subject: [PATCH 08/23] changed " to <> for #include --- tom/include/tom/config.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tom/include/tom/config.hpp b/tom/include/tom/config.hpp index a0a46d2ef..8e77359b6 100644 --- a/tom/include/tom/config.hpp +++ b/tom/include/tom/config.hpp @@ -2,7 +2,7 @@ #ifndef TOM_CONFIG_HPP #define TOM_CONFIG_HPP -#include "orm/macros/systemheader.hpp" +#include TINY_SYSTEM_HEADER // Check From 3595380cb958e175953ea109d768567c9a5f886e Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:36:32 +0200 Subject: [PATCH 09/23] added manager() getter to TinyUtils library --- tests/TinyUtils/src/databases.cpp | 24 +++++++++++++++++++++++- tests/TinyUtils/src/databases.hpp | 8 ++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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 & From acc5d1f0a2ac15522b7d0f5e02c8005f4e703dee Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:37:23 +0200 Subject: [PATCH 10/23] added QT *= core sql to TinyOrm.pri --- qmake/TinyOrm.pri | 1 + 1 file changed, 1 insertion(+) diff --git a/qmake/TinyOrm.pri b/qmake/TinyOrm.pri index 351dacbbe..c36eb2aa4 100644 --- a/qmake/TinyOrm.pri +++ b/qmake/TinyOrm.pri @@ -1,6 +1,7 @@ # TinyORM configuration # --- +QT *= core sql CONFIG *= c++2a strict_c++ warn_on utf8_source link_prl hide_symbols silent # Use extern constants for shared build From b9343e45d4ed9c7b1fee11e6a55c42a19b1b7e98 Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:42:37 +0200 Subject: [PATCH 11/23] bugfix shared_ptr for command definitions Commands in the blueprint don't have virtual dtors, they are not polymorphic, so shared_ptr has to be used because it can correctly destroy these aggregates. --- include/orm/schema/blueprint.hpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/orm/schema/blueprint.hpp b/include/orm/schema/blueprint.hpp index 7416a26b9..46c2ed2ac 100644 --- a/include/orm/schema/blueprint.hpp +++ b/include/orm/schema/blueprint.hpp @@ -390,7 +390,7 @@ namespace Grammars /*! Get the columns on the blueprint. */ inline const QVector &getColumns() const noexcept; /*! Get the commands on the blueprint. */ - inline const std::deque> & + inline const std::deque> & getCommands() const noexcept; /*! Determine whether the blueprint describes temporary table. */ inline bool isTemporary() const noexcept; @@ -425,7 +425,7 @@ namespace Grammars T &addCommand(T &&definition = {}); /*! Create a new Fluent command.. */ template - std::unique_ptr createCommand(T &&definition = {}); + std::shared_ptr createCommand(T &&definition = {}); /*! Add the commands that are implied by the blueprint's state. */ void addImpliedCommands(const SchemaGrammar &grammar); @@ -455,8 +455,9 @@ namespace Grammars /*! The columns that should be added to the table. */ QVector m_columns {}; + // BUG omg what dtors are called in this unique_ptr(), CommandDefinition doesn't have virtual dtor silverqx /*! The commands that should be run for the table. */ - std::deque> m_commands {}; + std::deque> m_commands {}; /*! Whether to make the table temporary. */ bool m_temporary = false; @@ -674,7 +675,7 @@ namespace Grammars return m_columns; } - const std::deque> & + const std::deque> & Blueprint::getCommands() const noexcept { return m_commands; @@ -703,10 +704,10 @@ namespace Grammars } template - std::unique_ptr + std::shared_ptr Blueprint::createCommand(T &&definition) { - return std::make_unique(std::forward(definition)); + return std::make_shared(std::forward(definition)); } } // namespace SchemaNs From 0290212d36d8aac64658fd53e5b1f237f2c4589b Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 13 Apr 2022 19:54:29 +0200 Subject: [PATCH 12/23] removed todo task --- include/orm/schema/blueprint.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/orm/schema/blueprint.hpp b/include/orm/schema/blueprint.hpp index 46c2ed2ac..e9ec54882 100644 --- a/include/orm/schema/blueprint.hpp +++ b/include/orm/schema/blueprint.hpp @@ -455,7 +455,6 @@ namespace Grammars /*! The columns that should be added to the table. */ QVector m_columns {}; - // BUG omg what dtors are called in this unique_ptr(), CommandDefinition doesn't have virtual dtor silverqx /*! The commands that should be run for the table. */ std::deque> m_commands {}; From a6213a6a9d3290fe4e70ead41d387fadc8426438 Mon Sep 17 00:00:00 2001 From: silverqx Date: Tue, 19 Apr 2022 20:02:37 +0200 Subject: [PATCH 13/23] =?UTF-8?q?added=20migrations=20=F0=9F=94=A5?= =?UTF-8?q?=F0=9F=9A=80=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Almost everything is implemented, possible to migrate up, down (rollback), migrate by steps (works for up/down too), reset, refresh, fresh, wipe database, showing status, installing migration table. Command line interface is superb, it supports ansi coloring, verbosity, no-interactive mode, force option, env option to select current env. It has enhanced ansi coloring (vt100 terminal) detection, when ansi or no-ansi option is not passed it can detect whether terminal supports coloring. Ansi coloring is disabled when redirection to file is detected (can be overridden by ansi/no-ansi options). Supports NO_COLOR env. variable (https://no-color.org/) and can detect the ConEmu, Hyper, and xterm on Windows. Carefully implemented help and list commands, list command can print supported commands by namespace. Advanced make migration command offers great command interface for creating migration classes, supports options for generating empty, create, or update migration classes. Unfinished make project command, will support creating qmake, qmake subproject, and cmake, cmake subproject projects. Later will be extracted to own executable tomproject.exe for rapidly generating a new TinyORM projects. Other implemented commands are env that prints current env. and inspire that displays an inspiring quote 😁. Verbose supports 5 levels quiet, normal, verbose, very verbose, and debug. Possibility to disable compilation of the tom command related code using TINYORM_DISABLE_TOM c macro, for the qmake exists disable_tom CONFIG option and for the CMake exist TOM configuration option. Confirmable interface that ask Y/N confirmation during migrate when env. == production, can be overridden by --force option. Whole tom terminal application supports or is implemented with UTF-8 support, also implemented UTF-16 output methods but they are not needed. Input also supports UTF-8, currently only Y/N input is needed by the Confirmation concern. All migrate commands also support the --pretend option and the --env option, when env. is production then tom asks confirmation to migrate, it can be overridden by the --force option. Added the tom example project, it is a complete command-line migration application, it uses migrations from the tests. Implementing this was really fun 🙃😎. - added 14 functional tests to test migrations up/down, stepping, installing migration table, refresh, reset on MySQL database - added unit test to check version number in tom.exe executable - new tom exception classes - created dozens of a new todo tasks 😂🤪, currently 348 todos 😎 - added some info messages to the qmake build about building features - in the debug build and with the -vvv option enable debugging of sql queries - enhanced RC and manifest file generation, allowed to pass a custom basename for a RC/manifest file as the 3. argument and a custom replace token for the CMake genex as the 4. argument - bugfix disabled #pragma code_page(65001) // UTF-8 in RC files, it messes up the © character Output of tom exe without arguments and options: Common options: --ansi Force ANSI output --no-ansi Disable ANSI output --env The environment the command should run under -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|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: env Display the current framework environment help Display help for a command inspire Display an inspiring quote list List commands migrate Run the database migrations db db:wipe Drop all tables, views, and types make make:migration Create a new migration file make:project Create a new Tom application project migrate migrate:fresh Drop all tables and re-run all migrations migrate:install Create the migration repository migrate:refresh Rollback and re-run all migrations migrate:reset Rollback all database migrations migrate:rollback Rollback the last database migration migrate:status Show the status of each migration --- CMakeLists.txt | 87 ++- TinyOrm.pro | 8 +- cmake/Modules/TinyInitDefaultVariables.cmake | 2 + cmake/Modules/TinySources.cmake | 104 +++- cmake/Modules/TinyTestCommon.cmake | 10 +- examples/CMakeLists.txt | 3 + examples/examples.pro | 7 + examples/tom/CMakeLists.txt | 74 +++ examples/tom/conf.pri.example | 39 ++ examples/tom/main.cpp | 99 +++ examples/tom/tom.pro | 53 ++ include/include.pri | 3 + include/orm/schema/schemabuilder.hpp | 2 +- include/orm/tiny/model.hpp | 10 + include/pch.h | 2 + qmake/TinyOrm.pri | 8 + qmake/common.pri | 50 +- qmake/features/tiny_resource_and_manifest.prf | 17 +- qmake/features/tiny_version_numbers.prf | 27 +- qmake/features/tom_example.prf | 2 + qmake/tom.pri | 54 ++ resources/TinyOrm.rc.in | 2 +- src/src.pri | 3 + src/src.pro | 24 +- tests/TinyUtils/resources/TinyUtils.rc.in | 2 +- tests/TinyUtils/src/pch.h | 4 +- tests/auto/functional/CMakeLists.txt | 4 + tests/auto/functional/functional.pro | 10 +- tests/auto/functional/tom/CMakeLists.txt | 1 + .../functional/tom/migrate/CMakeLists.txt | 18 + tests/auto/functional/tom/migrate/migrate.pro | 5 + .../functional/tom/migrate/tst_migrate.cpp | 585 ++++++++++++++++++ tests/auto/functional/tom/tom.pro | 4 + tests/auto/unit/orm/version/CMakeLists.txt | 9 + .../version/include/versiondebug_cmake.hpp.in | 1 + .../version/include/versiondebug_qmake.hpp.in | 1 + tests/auto/unit/orm/version/tst_version.cpp | 71 +++ tests/auto/unit/orm/version/version.pro | 9 + tests/database/migrations.pri | 7 + .../2014_10_12_000000_create_posts_table.hpp | 29 + ...00000_add_factor_column_to_posts_table.hpp | 29 + ...4_10_12_200000_create_properties_table.hpp | 29 + .../2014_10_12_300000_create_phones_table.hpp | 29 + tests/qmake/TinyOrm.pri | 9 +- tests/qmake/common.pri | 2 +- tests/resources/TinyTest.rc.in | 2 +- tom/include/include.pri | 33 + tom/include/pch.h | 45 -- tom/include/pch.pri | 8 - tom/include/tom/application.hpp | 264 ++++++++ tom/include/tom/commands/command.hpp | 153 +++++ .../tom/commands/database/wipecommand.hpp | 67 ++ .../tom/commands/environmentcommand.hpp | 51 ++ tom/include/tom/commands/helpcommand.hpp | 85 +++ tom/include/tom/commands/inspirecommand.hpp | 51 ++ tom/include/tom/commands/listcommand.hpp | 89 +++ .../tom/commands/make/migrationcommand.hpp | 80 +++ .../tom/commands/make/projectcommand.hpp | 78 +++ .../commands/make/stubs/migrationstubs.hpp | 110 ++++ .../tom/commands/make/stubs/projectstubs.hpp | 22 + .../tom/commands/migrations/freshcommand.hpp | 74 +++ .../commands/migrations/installcommand.hpp | 64 ++ .../commands/migrations/migratecommand.hpp | 79 +++ .../commands/migrations/refreshcommand.hpp | 74 +++ .../tom/commands/migrations/resetcommand.hpp | 69 +++ .../commands/migrations/rollbackcommand.hpp | 69 +++ .../tom/commands/migrations/statuscommand.hpp | 118 ++++ tom/include/tom/concerns/callscommands.hpp | 67 ++ tom/include/tom/concerns/confirmable.hpp | 58 ++ tom/include/tom/concerns/interactswithio.hpp | 238 +++++++ tom/include/tom/concerns/printsoptions.hpp | 53 ++ .../tom/exceptions/invalidargumenterror.hpp | 26 + .../invalidtemplateargumenterror.hpp | 26 + tom/include/tom/exceptions/logicerror.hpp | 48 ++ tom/include/tom/exceptions/runtimeerror.hpp | 48 ++ tom/include/tom/exceptions/tomerror.hpp | 27 + tom/include/tom/migration.hpp | 131 ++++ tom/include/tom/migrationcreator.hpp | 65 ++ tom/include/tom/migrationrepository.hpp | 94 +++ tom/include/tom/migrator.hpp | 178 ++++++ tom/include/tom/terminal.hpp | 129 ++++ tom/include/tom/tomtypes.hpp | 46 ++ tom/resources/tom.rc.in | 8 +- tom/src/src.pri | 27 +- tom/src/src.pro | 95 --- tom/src/tom/application.cpp | 474 ++++++++++++++ tom/src/tom/commands/command.cpp | 198 ++++++ tom/src/tom/commands/database/wipecommand.cpp | 80 +++ tom/src/tom/commands/environmentcommand.cpp | 30 + tom/src/tom/commands/helpcommand.cpp | 240 +++++++ tom/src/tom/commands/inspirecommand.cpp | 80 +++ tom/src/tom/commands/listcommand.cpp | 234 +++++++ .../tom/commands/make/migrationcommand.cpp | 99 +++ tom/src/tom/commands/make/projectcommand.cpp | 95 +++ .../tom/commands/migrations/freshcommand.cpp | 85 +++ .../commands/migrations/installcommand.cpp | 51 ++ .../commands/migrations/migratecommand.cpp | 100 +++ .../commands/migrations/refreshcommand.cpp | 87 +++ .../tom/commands/migrations/resetcommand.cpp | 61 ++ .../commands/migrations/rollbackcommand.cpp | 58 ++ .../tom/commands/migrations/statuscommand.cpp | 133 ++++ tom/src/tom/concerns/callscommands.cpp | 104 ++++ tom/src/tom/concerns/confirmable.cpp | 63 ++ tom/src/tom/concerns/interactswithio.cpp | 509 +++++++++++++++ tom/src/tom/concerns/printsoptions.cpp | 103 +++ tom/src/tom/exceptions/tomlogicerror.cpp | 22 + tom/src/tom/exceptions/tomruntimeerror.cpp | 22 + tom/src/tom/main.cpp | 68 -- tom/src/tom/migrationcreator.cpp | 147 +++++ tom/src/tom/migrationrepository.cpp | 190 ++++++ tom/src/tom/migrator.cpp | 415 +++++++++++++ tom/src/tom/terminal.cpp | 212 +++++++ tom/tom.pro | 9 - 113 files changed, 8276 insertions(+), 290 deletions(-) create mode 100644 examples/CMakeLists.txt create mode 100644 examples/examples.pro create mode 100644 examples/tom/CMakeLists.txt create mode 100644 examples/tom/conf.pri.example create mode 100644 examples/tom/main.cpp create mode 100644 examples/tom/tom.pro create mode 100644 qmake/features/tom_example.prf create mode 100644 qmake/tom.pri create mode 100644 tests/auto/functional/tom/CMakeLists.txt create mode 100644 tests/auto/functional/tom/migrate/CMakeLists.txt create mode 100644 tests/auto/functional/tom/migrate/migrate.pro create mode 100644 tests/auto/functional/tom/migrate/tst_migrate.cpp create mode 100644 tests/auto/functional/tom/tom.pro create mode 100644 tests/database/migrations.pri create mode 100644 tests/database/migrations/2014_10_12_000000_create_posts_table.hpp create mode 100644 tests/database/migrations/2014_10_12_100000_add_factor_column_to_posts_table.hpp create mode 100644 tests/database/migrations/2014_10_12_200000_create_properties_table.hpp create mode 100644 tests/database/migrations/2014_10_12_300000_create_phones_table.hpp delete mode 100644 tom/include/pch.h delete mode 100644 tom/include/pch.pri create mode 100644 tom/include/tom/application.hpp create mode 100644 tom/include/tom/commands/command.hpp create mode 100644 tom/include/tom/commands/database/wipecommand.hpp create mode 100644 tom/include/tom/commands/environmentcommand.hpp create mode 100644 tom/include/tom/commands/helpcommand.hpp create mode 100644 tom/include/tom/commands/inspirecommand.hpp create mode 100644 tom/include/tom/commands/listcommand.hpp create mode 100644 tom/include/tom/commands/make/migrationcommand.hpp create mode 100644 tom/include/tom/commands/make/projectcommand.hpp create mode 100644 tom/include/tom/commands/make/stubs/migrationstubs.hpp create mode 100644 tom/include/tom/commands/make/stubs/projectstubs.hpp create mode 100644 tom/include/tom/commands/migrations/freshcommand.hpp create mode 100644 tom/include/tom/commands/migrations/installcommand.hpp create mode 100644 tom/include/tom/commands/migrations/migratecommand.hpp create mode 100644 tom/include/tom/commands/migrations/refreshcommand.hpp create mode 100644 tom/include/tom/commands/migrations/resetcommand.hpp create mode 100644 tom/include/tom/commands/migrations/rollbackcommand.hpp create mode 100644 tom/include/tom/commands/migrations/statuscommand.hpp create mode 100644 tom/include/tom/concerns/callscommands.hpp create mode 100644 tom/include/tom/concerns/confirmable.hpp create mode 100644 tom/include/tom/concerns/interactswithio.hpp create mode 100644 tom/include/tom/concerns/printsoptions.hpp create mode 100644 tom/include/tom/exceptions/invalidargumenterror.hpp create mode 100644 tom/include/tom/exceptions/invalidtemplateargumenterror.hpp create mode 100644 tom/include/tom/exceptions/logicerror.hpp create mode 100644 tom/include/tom/exceptions/runtimeerror.hpp create mode 100644 tom/include/tom/exceptions/tomerror.hpp create mode 100644 tom/include/tom/migration.hpp create mode 100644 tom/include/tom/migrationcreator.hpp create mode 100644 tom/include/tom/migrationrepository.hpp create mode 100644 tom/include/tom/migrator.hpp create mode 100644 tom/include/tom/terminal.hpp create mode 100644 tom/include/tom/tomtypes.hpp delete mode 100644 tom/src/src.pro create mode 100644 tom/src/tom/application.cpp create mode 100644 tom/src/tom/commands/command.cpp create mode 100644 tom/src/tom/commands/database/wipecommand.cpp create mode 100644 tom/src/tom/commands/environmentcommand.cpp create mode 100644 tom/src/tom/commands/helpcommand.cpp create mode 100644 tom/src/tom/commands/inspirecommand.cpp create mode 100644 tom/src/tom/commands/listcommand.cpp create mode 100644 tom/src/tom/commands/make/migrationcommand.cpp create mode 100644 tom/src/tom/commands/make/projectcommand.cpp create mode 100644 tom/src/tom/commands/migrations/freshcommand.cpp create mode 100644 tom/src/tom/commands/migrations/installcommand.cpp create mode 100644 tom/src/tom/commands/migrations/migratecommand.cpp create mode 100644 tom/src/tom/commands/migrations/refreshcommand.cpp create mode 100644 tom/src/tom/commands/migrations/resetcommand.cpp create mode 100644 tom/src/tom/commands/migrations/rollbackcommand.cpp create mode 100644 tom/src/tom/commands/migrations/statuscommand.cpp create mode 100644 tom/src/tom/concerns/callscommands.cpp create mode 100644 tom/src/tom/concerns/confirmable.cpp create mode 100644 tom/src/tom/concerns/interactswithio.cpp create mode 100644 tom/src/tom/concerns/printsoptions.cpp create mode 100644 tom/src/tom/exceptions/tomlogicerror.cpp create mode 100644 tom/src/tom/exceptions/tomruntimeerror.cpp delete mode 100644 tom/src/tom/main.cpp create mode 100644 tom/src/tom/migrationcreator.cpp create mode 100644 tom/src/tom/migrationrepository.cpp create mode 100644 tom/src/tom/migrator.cpp create mode 100644 tom/src/tom/terminal.cpp delete mode 100644 tom/tom.pro 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 67d2d3a3c..119bf0cec 100644 --- a/TinyOrm.pro +++ b/TinyOrm.pro @@ -2,13 +2,15 @@ TEMPLATE = subdirs SUBDIRS = src -!disable_tom { - SUBDIRS += tom - tom.depends = 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/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/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/model.hpp b/include/orm/tiny/model.hpp index ee46e1e1b..c8b357288 100644 --- a/include/orm/tiny/model.hpp +++ b/include/orm/tiny/model.hpp @@ -1338,3 +1338,13 @@ TINYORM_END_COMMON_NAMESPACE // 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/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/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/resources/TinyOrm.rc.in b/resources/TinyOrm.rc.in index cff4bf3b4..dfc41b34f 100644 --- a/resources/TinyOrm.rc.in +++ b/resources/TinyOrm.rc.in @@ -1,4 +1,4 @@ -#pragma code_page(65001) // UTF-8 +//#pragma code_page(65001) // UTF-8 IDI_ICON1 ICON "icons/@TinyOrm_target@.ico" 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/resources/TinyUtils.rc.in b/tests/TinyUtils/resources/TinyUtils.rc.in index a4926cb22..0e4f798e2 100644 --- a/tests/TinyUtils/resources/TinyUtils.rc.in +++ b/tests/TinyUtils/resources/TinyUtils.rc.in @@ -1,4 +1,4 @@ -#pragma code_page(65001) // UTF-8 +//#pragma code_page(65001) // UTF-8 //IDI_ICON1 ICON "icons/@TinyUtils_target@.ico" 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 671afb4ab..e110d783b 100644 --- a/tests/auto/unit/orm/version/tst_version.cpp +++ b/tests/auto/unit/orm/version/tst_version.cpp @@ -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/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/qmake/TinyOrm.pri b/tests/qmake/TinyOrm.pri index 6bebbff25..3b5474ad5 100644 --- a/tests/qmake/TinyOrm.pri +++ b/tests/qmake/TinyOrm.pri @@ -5,14 +5,17 @@ CONFIG(shared, dll|shared|static|staticlib) | \ CONFIG(dll, dll|shared|static|staticlib): \ DEFINES += TINYORM_LINKING_SHARED -# Enable code needed by tests, eg connection overriding in the Model -DEFINES *= TINYORM_TESTS_CODE - # TinyORM library headers include path # --- 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 065b66759..954cb5160 100644 --- a/tests/qmake/common.pri +++ b/tests/qmake/common.pri @@ -45,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/tests/resources/TinyTest.rc.in b/tests/resources/TinyTest.rc.in index 527ac17b3..90441e0ba 100644 --- a/tests/resources/TinyTest.rc.in +++ b/tests/resources/TinyTest.rc.in @@ -1,4 +1,4 @@ -#pragma code_page(65001) // UTF-8 +//#pragma code_page(65001) // UTF-8 //IDI_ICON1 ICON "icons/@TinyTest_icon@.ico" diff --git a/tom/include/include.pri b/tom/include/include.pri index f3f8ad892..74868ca81 100644 --- a/tom/include/include.pri +++ b/tom/include/include.pri @@ -1,5 +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/pch.h b/tom/include/pch.h deleted file mode 100644 index ac3d59ed6..000000000 --- a/tom/include/pch.h +++ /dev/null @@ -1,45 +0,0 @@ -/* This file can't be included in the project, it's for a precompiled header. */ - -/* Add C includes here */ - -#if defined __cplusplus -/* Add C++ includes here */ -//#include -//#include -//#include -//#include -#include -//#include -//#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -//#include -#include -#include -#include -#include -#include -#include -#endif diff --git a/tom/include/pch.pri b/tom/include/pch.pri deleted file mode 100644 index 62b027493..000000000 --- a/tom/include/pch.pri +++ /dev/null @@ -1,8 +0,0 @@ -# Use Precompiled headers (PCH) -# --- - -PRECOMPILED_HEADER = $$quote($$PWD/pch.h) -HEADERS += $$PRECOMPILED_HEADER - -precompile_header: \ - DEFINES *= USING_PCH diff --git a/tom/include/tom/application.hpp b/tom/include/tom/application.hpp new file mode 100644 index 000000000..c95b07b11 --- /dev/null +++ b/tom/include/tom/application.hpp @@ -0,0 +1,264 @@ +#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; + + /* Application initialization */ +#ifdef _WIN32 + /*! Prepare console input/output character encoding. */ + void initializeConsoleEncoding() const; +#endif + /*! 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..6a96abf82 --- /dev/null +++ b/tom/include/tom/commands/make/migrationcommand.hpp @@ -0,0 +1,80 @@ +#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 path = 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). */ + inline path 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"); + } + + /* protected */ + + // CUR tom, finish --path/--realpath silverqx + std::filesystem::path MigrationCommand::getMigrationPath() const + { + return path(__FILE__).parent_path().parent_path().parent_path() / "migrations"; + } + +} // 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..ad218cc46 --- /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; + /*! Default destructor. */ + inline ~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..a7a9464f9 --- /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); + /*! Default destructor. */ + inline ~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..4da140bd8 --- /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); + /*! Default destructor. */ + ~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..849487348 --- /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); + /*! Default destructor. */ + inline ~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/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..43c6baa11 --- /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(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..a9244d26b --- /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..600dea5cf --- /dev/null +++ b/tom/include/tom/terminal.hpp @@ -0,0 +1,129 @@ +#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; + + /*! 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 + + /*! 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 + { + // 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/resources/tom.rc.in b/tom/resources/tom.rc.in index 74930faa0..188e49741 100644 --- a/tom/resources/tom.rc.in +++ b/tom/resources/tom.rc.in @@ -1,6 +1,6 @@ //#pragma code_page(65001) // UTF-8 -//IDI_ICON1 ICON "icons/@tom_target@.ico" +//IDI_ICON1 ICON "icons/@TomExample_target@.ico" #include #include "tom/version.hpp" @@ -11,7 +11,7 @@ #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" +#define VER_ORIGINALFILENAME_STR "$\0" #ifdef TINYTOM_NO_DEBUG # define VER_DEBUG 0 @@ -33,7 +33,7 @@ VS_VERSION_INFO VERSIONINFO BLOCK "040904B0" BEGIN VALUE "CompanyName", "Crystal Studio\0" - VALUE "FileDescription", "tom console for TinyORM\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" @@ -51,5 +51,5 @@ VS_VERSION_INFO VERSIONINFO /* End of Version info */ #ifdef __MINGW32__ -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "@tom_target@$.manifest" +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "@TomExample_target@$.manifest" #endif diff --git a/tom/src/src.pri b/tom/src/src.pri index d0023c38d..e8601efa5 100644 --- a/tom/src/src.pri +++ b/tom/src/src.pri @@ -1,2 +1,27 @@ SOURCES += \ - $$PWD/tom/main.cpp \ + $$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/src.pro b/tom/src/src.pro deleted file mode 100644 index 66a975bad..000000000 --- a/tom/src/src.pro +++ /dev/null @@ -1,95 +0,0 @@ -QT *= core sql -QT -= gui - -TEMPLATE = app -TARGET = tom - -# TinyTom application specific configuration -# --- - -CONFIG *= console - -# Common Configuration -# --- - -include($$TINYORM_SOURCE_TREE/qmake/common.pri) - -# TinyTom defines -# --- - -DEFINES += PROJECT_TINYTOM - -# Release build -CONFIG(release, debug|release): DEFINES += TINYTOM_NO_DEBUG -# Debug build -CONFIG(debug, debug|release): DEFINES *= TINYTOM_DEBUG - -# Enable code needed by tests -#build_tests: \ -# DEFINES *= TINYTOM_TESTS_CODE - -# TinyTom library header and source files -# --- - -# tiny_version_numbers() depends on HEADERS (version.hpp) -include(../include/include.pri) -include(src.pri) - -# 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($$PWD/../include/) -# Find Windows manifest -mingw: tinyRcIncludepath += $$quote($$PWD/../resources/) - -load(tiny_resource_and_manifest) -tiny_resource_and_manifest($$tinyRcIncludepath, ../resources) - -# Use Precompiled headers (PCH) -# --- - -include(../include/pch.pri) - -# 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 -} - -# Configure TinyORM library -# --- - -include($$TINYORM_SOURCE_TREE/qmake/TinyOrm.pri) - -# User Configuration -# --- - -exists(../conf.pri): \ - include(../conf.pri) - -#else:is_vcpkg_build: \ -# include(../qmake/vcpkgconf.pri) - -else: \ - error( "'conf.pri' for 'tom' project does not exist. See an example configuration\ - in 'tom/conf.pri.example' or call 'vcpkg install' in the project's root." ) - -# Link against TinyORM library -# --- - -INCLUDEPATH += $$quote($$TINYORM_SOURCE_TREE/include/) - -LIBS += $$quote(-L$$TINYORM_BUILD_TREE/src$${TINY_RELEASE_TYPE}/) -LIBS += -lTinyOrm diff --git a/tom/src/tom/application.cpp b/tom/src/tom/application.cpp new file mode 100644 index 000000000..c8f5602c6 --- /dev/null +++ b/tom/src/tom/application.cpp @@ -0,0 +1,474 @@ +#include "tom/application.hpp" + +#include +#include + +#ifdef _WIN32 +# include + +# include +# include +#endif + +#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" +#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 { + +/* 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)) +{ +#ifdef _WIN32 + // Prepare console input/output character encoding + initializeConsoleEncoding(); +#endif + + // 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 */ + +/* Application initialization */ + +#ifdef _WIN32 +void Application::initializeConsoleEncoding() const +{ + // 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 + +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..a60afc87b --- /dev/null +++ b/tom/src/tom/commands/listcommand.cpp @@ -0,0 +1,234 @@ +#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. + 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, 8)}, + {"migrate", std::make_tuple(8, 14)}, + }; + + 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..515eb667e --- /dev/null +++ b/tom/src/tom/commands/make/migrationcommand.cpp @@ -0,0 +1,99 @@ +#include "tom/commands/make/migrationcommand.hpp" + +#include + +#include +#include + +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::fromWCharArray(migrationFile.c_str())); +} + +} // 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/main.cpp b/tom/src/tom/main.cpp deleted file mode 100644 index e0e4dfece..000000000 --- a/tom/src/tom/main.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include - -#if defined(_WIN32) -#include -#endif - -#include - -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_; - -int main(int /*unused*/, char */*unused*/[]) -//int main(int argc, char *argv[]) -{ -// QCoreApplication app(argc, argv); -#ifdef _WIN32 - SetConsoleOutputCP(CP_UTF8); -// SetConsoleOutputCP(1250); -#endif - - QCoreApplication::setOrganizationName("TinyORM"); - QCoreApplication::setOrganizationDomain("tinyorm.org"); - QCoreApplication::setApplicationName("tom"); - - qDebug() << "tom"; - - auto dm = Orm::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"))}, - // CUR add timezone names to the MySQL server and test them silverqx - {timezone_, SYSTEM}, - {prefix_, ""}, - {prefix_indexes, true}, - {strict_, true}, - {isolation_level, QStringLiteral("REPEATABLE READ")}, - {engine_, InnoDB}, - {options_, QVariantHash()}, - }); - -// return QCoreApplication::exec(); - return 0; -} diff --git a/tom/src/tom/migrationcreator.cpp b/tom/src/tom/migrationcreator.cpp new file mode 100644 index 000000000..f1df243be --- /dev/null +++ b/tom/src/tom/migrationcreator.cpp @@ -0,0 +1,147 @@ +#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(migrationPath.parent_path()); + + // 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 +{ + using options = fs::directory_options; + + for (const auto &entry : + fs::directory_iterator(migrationsPath, options::skip_permission_denied) + ) { + // Extract migration name without datetime prefix and extension + auto entryName = QString::fromWCharArray(entry.path().filename().c_str()) + .mid(DatePrefix->size() + 1); + + entryName.truncate(entryName.lastIndexOf(DOT)); + + if (entry.is_regular_file() && 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(fspath &&path) const +{ + if (fs::is_directory(path) && fs::exists(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..36415ec65 --- /dev/null +++ b/tom/src/tom/terminal.cpp @@ -0,0 +1,212 @@ +#include "tom/terminal.hpp" + +#ifdef _WIN32 +# include +#elif defined(__linux__) +# include +#endif + +#include + +#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 😎 + +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 +{ + // CUR tom, linux silverqx + return _isatty(_fileno(stream)) != 0; +} + +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; +} +#endif + +} // namespace Tom + +TINYORM_END_COMMON_NAMESPACE diff --git a/tom/tom.pro b/tom/tom.pro deleted file mode 100644 index ef431d5d9..000000000 --- a/tom/tom.pro +++ /dev/null @@ -1,9 +0,0 @@ -TEMPLATE = subdirs - -SUBDIRS = src - -# Can be enabled by CONFIG += build_tests when the qmake.exe for the project is called -#build_tests { -# SUBDIRS += tests -# tests.depends = src -#} From 9a91645df0f6eb2f7d29d7267b76ff7776c5512f Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 20 Apr 2022 15:46:13 +0200 Subject: [PATCH 14/23] bugfix in String::splitStringByWidth() --- src/orm/tiny/utils/string.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/orm/tiny/utils/string.cpp b/src/orm/tiny/utils/string.cpp index 5f1cb94a7..2f1980ffa 100644 --- a/src/orm/tiny/utils/string.cpp +++ b/src/orm/tiny/utils/string.cpp @@ -173,6 +173,10 @@ std::vector String::splitStringByWidth(const QString &string, const int /* Section - compute how much chars to copy */ QString::size_type copySize = -1; + // No space found in the current range, eg. long path so copy whole searched block + if (pos < from) + pos = searchFrom; + // The last text block, -1 for copy the rest if (isEnd) copySize = -1; @@ -206,7 +210,8 @@ std::vector String::splitStringByWidth(const QString &string, const int /* +1 means - don't copy a space at beginning (skip space at beginning), is guaranteed that the first char will be a space. */ else - from = pos + 1; + // Don't skip space if no space found in the searched block + from = pos == searchFrom ? pos : pos + 1; } return lines; From f12dd24ef4db80ed5a450dbeab399281b5fdd5df Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 20 Apr 2022 17:04:20 +0200 Subject: [PATCH 15/23] bugfix replaced String::splitStringByWidth() Replaced algorithm that drives String::splitStringByWidth(), it was very bugprone. --- src/orm/tiny/utils/string.cpp | 123 +++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/src/orm/tiny/utils/string.cpp b/src/orm/tiny/utils/string.cpp index 2f1980ffa..9abf606e9 100644 --- a/src/orm/tiny/utils/string.cpp +++ b/src/orm/tiny/utils/string.cpp @@ -150,70 +150,83 @@ bool String::isNumber(const QString &string, const bool allowFloating) /*! Split a string by the given width (not in the middle of a word). */ std::vector String::splitStringByWidth(const QString &string, const int width) { - const auto stringSize = string.size(); - // Nothing to split - if (stringSize <= width) + if (string.size() <= width) return {string}; - QString::size_type from = 0; - std::vector lines; + QString line; - while (true) { - /* Section - find split position */ - auto searchFrom = from + width - 1; + for (auto token : string.tokenize(SPACE)) { + /* 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(' '); - // Hit the end - the last text block - !(searchFrom < stringSize) - const auto isEnd = searchFrom >= stringSize - 1; - - // Returns pos == -1 if not found - auto pos = string.lastIndexOf(SPACE, isEnd ? -1 : searchFrom); - - /* Section - compute how much chars to copy */ - QString::size_type copySize = -1; - - // No space found in the current range, eg. long path so copy whole searched block - if (pos < from) - pos = searchFrom; - - // The last text block, -1 for copy the rest - if (isEnd) - copySize = -1; - - // If pos == -1 (no space found) and not at end then copy a whole width block - else if (pos == -1 && !isEnd) - copySize = width; - - // Copy to the found space char - else - copySize = pos - from; - - /* Section - done, copy a text */ - lines.emplace_back(string.mid(from, copySize)); - - // Hit the end - the last text block - !(searchFrom < stringSize) - if (isEnd) - break; - - /* Section - prepare 'from' for the next loop */ - /* Start after the whole width block (if pos == -1 then pos + 1 is not - correct here). */ - if (pos == -1) { - from += width; - - // When whole block was copied, the next char can or can not be a space - if (string.at(from) == SPACE) - ++from; + line.append(token); + continue; } - // Start from a last found space - /* +1 means - don't copy a space at beginning (skip space at beginning), - is guaranteed that the first char will be a space. */ - else - // Don't skip space if no space found in the searched block - from = pos == searchFrom ? pos : pos + 1; + + /* If a token is longer than the width or an empty space on the current line */ + 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 > llround(static_cast(width) * 0.3)) { + // 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(); + } + + 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 From 8c4990c111ca3c4468922bb510e76df13c33818c Mon Sep 17 00:00:00 2001 From: silverqx Date: Wed, 20 Apr 2022 17:11:58 +0200 Subject: [PATCH 16/23] tom finished make:migration command Finished --path and --realpath options. - enhanced logic for checking whether a migration already exists --- .../tom/commands/make/migrationcommand.hpp | 12 ++------ tom/include/tom/migrationcreator.hpp | 2 +- .../tom/commands/make/migrationcommand.cpp | 28 +++++++++++++++++++ tom/src/tom/migrationcreator.cpp | 16 ++++++++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/tom/include/tom/commands/make/migrationcommand.hpp b/tom/include/tom/commands/make/migrationcommand.hpp index 6a96abf82..4980f5568 100644 --- a/tom/include/tom/commands/make/migrationcommand.hpp +++ b/tom/include/tom/commands/make/migrationcommand.hpp @@ -19,7 +19,7 @@ namespace Tom::Commands::Make Q_DISABLE_COPY(MigrationCommand) /*! Alias for the filesystem path. */ - using path = std::filesystem::path; + using fspath = std::filesystem::path; public: /*! Constructor. */ @@ -47,7 +47,7 @@ namespace Tom::Commands::Make /*! Get migration path (either specified by '--path' option or default location). */ - inline path getMigrationPath() const; + fspath getMigrationPath() const; /*! The migration creator instance. */ MigrationCreator m_creator {}; @@ -65,14 +65,6 @@ namespace Tom::Commands::Make return QLatin1String("Create a new migration file"); } - /* protected */ - - // CUR tom, finish --path/--realpath silverqx - std::filesystem::path MigrationCommand::getMigrationPath() const - { - return path(__FILE__).parent_path().parent_path().parent_path() / "migrations"; - } - } // namespace Tom::Commands::Make TINYORM_END_COMMON_NAMESPACE diff --git a/tom/include/tom/migrationcreator.hpp b/tom/include/tom/migrationcreator.hpp index 43c6baa11..76cabba5b 100644 --- a/tom/include/tom/migrationcreator.hpp +++ b/tom/include/tom/migrationcreator.hpp @@ -55,7 +55,7 @@ namespace Tom /*! Get the class name of a migration name. */ QString getClassName(const QString &name) const; /*! Ensure a directory exists. */ - void ensureDirectoryExists(fspath &&path) const; + void ensureDirectoryExists(const fspath &path) const; }; } // namespace Tom diff --git a/tom/src/tom/commands/make/migrationcommand.cpp b/tom/src/tom/commands/make/migrationcommand.cpp index 515eb667e..17494d457 100644 --- a/tom/src/tom/commands/make/migrationcommand.cpp +++ b/tom/src/tom/commands/make/migrationcommand.cpp @@ -5,6 +5,10 @@ #include #include +#include "tom/exceptions/invalidargumenterror.hpp" + +namespace fs = std::filesystem; + using fspath = std::filesystem::path; using Orm::Constants::NAME; @@ -94,6 +98,30 @@ void MigrationCommand::writeMigration(const QString &name, const QString &table, note(QString::fromWCharArray(migrationFile.c_str())); } +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/migrationcreator.cpp b/tom/src/tom/migrationcreator.cpp index f1df243be..a45162cd7 100644 --- a/tom/src/tom/migrationcreator.cpp +++ b/tom/src/tom/migrationcreator.cpp @@ -41,7 +41,7 @@ fspath MigrationCreator::create(const QString &name, fspath &&migrationsPath, various place-holders, and save the file. */ auto stub = getStub(table, create); - ensureDirectoryExists(migrationPath.parent_path()); + ensureDirectoryExists(migrationsPath); // Output it as binary stream to force line endings to LF std::ofstream(migrationPath, std::ios::out | std::ios::binary) @@ -61,18 +61,26 @@ namespace 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::fromWCharArray(entry.path().filename().c_str()) .mid(DatePrefix->size() + 1); entryName.truncate(entryName.lastIndexOf(DOT)); - if (entry.is_regular_file() && entryName == name) + if (entryName == name) throw Exceptions::InvalidArgumentError( QStringLiteral("A '%1' migration already exists.").arg(name)); } @@ -134,9 +142,9 @@ QString MigrationCreator::getClassName(const QString &name) const return StringUtils::studly(name); } -void MigrationCreator::ensureDirectoryExists(fspath &&path) const +void MigrationCreator::ensureDirectoryExists(const fspath &path) const { - if (fs::is_directory(path) && fs::exists(path)) + if (fs::exists(path) && fs::is_directory(path)) return; fs::create_directories(path); From 39f8098932d1e9da1f094095b954dedbbfd98415 Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 10:11:17 +0200 Subject: [PATCH 17/23] added support for MSYS2 gcc/clang --- src/orm/tiny/utils/string.cpp | 75 ++++++++++++++------ tom/include/tom/concerns/callscommands.hpp | 4 +- tom/include/tom/concerns/confirmable.hpp | 4 +- tom/include/tom/concerns/interactswithio.hpp | 4 +- tom/include/tom/concerns/printsoptions.hpp | 4 +- tom/include/tom/migrator.hpp | 2 +- tom/include/tom/terminal.hpp | 4 +- 7 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/orm/tiny/utils/string.cpp b/src/orm/tiny/utils/string.cpp index 9abf606e9..2ae54dc26 100644 --- a/src/orm/tiny/utils/string.cpp +++ b/src/orm/tiny/utils/string.cpp @@ -1,5 +1,9 @@ #include "orm/tiny/utils/string.hpp" +#include + +#include + #include "orm/constants.hpp" using Orm::Constants::DASH; @@ -28,7 +32,7 @@ namespace using SnakeCache = std::unordered_map; /*! Snake cache for already computed strings. */ - Q_GLOBAL_STATIC(SnakeCache, snakeCache); + Q_GLOBAL_STATIC(SnakeCache, snakeCache); // NOLINT(readability-redundant-member-init) } // namespace QString String::snake(QString string, const QChar delimiter) @@ -70,7 +74,7 @@ namespace using StudlyCache = std::unordered_map; /*! Studly cache for already computed strings. */ - Q_GLOBAL_STATIC(StudlyCache, studlyCache); + Q_GLOBAL_STATIC(StudlyCache, studlyCache); // NOLINT(readability-redundant-member-init) } // namespace QString String::studly(QString string) @@ -147,35 +151,27 @@ bool String::isNumber(const QString &string, const bool allowFloating) } #ifndef TINYORM_DISABLE_TOM -/*! Split a string by the given width (not in the middle of a word). */ -std::vector String::splitStringByWidth(const QString &string, const int width) +namespace { - // Nothing to split - if (string.size() <= width) - return {string}; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + using StringViewType = QStringView; +#else + using StringViewType = QStringRef; +#endif - std::vector lines; - QString line; + /*! 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; - for (auto token : string.tokenize(SPACE)) { - /* 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(' '); - - line.append(token); - continue; - } - - /* If a token is longer than the width or an empty space on the current line */ 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 > llround(static_cast(width) * 0.3)) { + if (emptySpace > std::llround(static_cast(width) * 0.3F)) { // Position where to split the token auto pos = width - line.size() - spaceSize; @@ -208,12 +204,45 @@ std::vector String::splitStringByWidth(const QString &string, const int // Push to lines lines.emplace_back(std::move(line)); // Start a new line - line.clear(); + 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)); diff --git a/tom/include/tom/concerns/callscommands.hpp b/tom/include/tom/concerns/callscommands.hpp index ad218cc46..1acfd67b5 100644 --- a/tom/include/tom/concerns/callscommands.hpp +++ b/tom/include/tom/concerns/callscommands.hpp @@ -29,8 +29,8 @@ namespace Concerns public: /*! Default constructor. */ inline CallsCommands() = default; - /*! Default destructor. */ - inline ~CallsCommands() = default; + /*! Virtual destructor. */ + inline virtual ~CallsCommands() = default; /*! Call another console command. */ inline int call(const QString &command, QStringList &&arguments = {}) const; diff --git a/tom/include/tom/concerns/confirmable.hpp b/tom/include/tom/concerns/confirmable.hpp index a7a9464f9..f11255ec8 100644 --- a/tom/include/tom/concerns/confirmable.hpp +++ b/tom/include/tom/concerns/confirmable.hpp @@ -34,8 +34,8 @@ namespace Concerns public: /*! Constructor (int param. to avoid interpret it as copy ctor). */ Confirmable(Command &command, int); - /*! Default destructor. */ - inline ~Confirmable() = default; + /*! Virtual destructor. */ + inline virtual ~Confirmable() = default; /*! Confirm before proceeding with the action (only in production environment). */ bool confirmToProceed( diff --git a/tom/include/tom/concerns/interactswithio.hpp b/tom/include/tom/concerns/interactswithio.hpp index 4da140bd8..6107ba6b6 100644 --- a/tom/include/tom/concerns/interactswithio.hpp +++ b/tom/include/tom/concerns/interactswithio.hpp @@ -40,8 +40,8 @@ namespace Concerns /*! Constructor. */ explicit InteractsWithIO(const QCommandLineParser &parser); - /*! Default destructor. */ - ~InteractsWithIO(); + /*! Virtual destructor. */ + virtual ~InteractsWithIO(); /*! Base enum for the verbosity levels. */ enum struct Verbosity { diff --git a/tom/include/tom/concerns/printsoptions.hpp b/tom/include/tom/concerns/printsoptions.hpp index 849487348..f70e89bfe 100644 --- a/tom/include/tom/concerns/printsoptions.hpp +++ b/tom/include/tom/concerns/printsoptions.hpp @@ -29,8 +29,8 @@ namespace Concerns public: /*! Constructor (int param. to avoid interpret it as copy ctor). */ PrintsOptions(const Commands::Command &command, int); - /*! Default destructor. */ - inline ~PrintsOptions() = default; + /*! Virtual destructor. */ + inline virtual ~PrintsOptions() = default; /*! Print options section. */ int printOptionsSection(bool commonOptions) const; diff --git a/tom/include/tom/migrator.hpp b/tom/include/tom/migrator.hpp index a9244d26b..c0e15db4b 100644 --- a/tom/include/tom/migrator.hpp +++ b/tom/include/tom/migrator.hpp @@ -139,7 +139,7 @@ namespace Tom /*! The database connection resolver instance. */ std::shared_ptr m_resolver; /*! The name of the database connection to use. */ - QString m_connection; + QString m_connection {}; /*! Reference to the migrations vector to process. */ const std::vector> &m_migrations; diff --git a/tom/include/tom/terminal.hpp b/tom/include/tom/terminal.hpp index 600dea5cf..795c4b6eb 100644 --- a/tom/include/tom/terminal.hpp +++ b/tom/include/tom/terminal.hpp @@ -76,9 +76,9 @@ namespace Tom #endif /*! Cache for detected ansi output. */ - mutable std::unordered_map m_isAnsiOutput; + mutable std::unordered_map m_isAnsiOutput {}; /*! Cache for detected ansi output, wide version. */ - mutable std::unordered_map m_isAnsiWOutput; + mutable std::unordered_map m_isAnsiWOutput {}; /*! Current terminal width. */ int m_lastWidth = 80; From a43b48a7be812f39856d8e7326767122beac2d35 Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 10:23:41 +0200 Subject: [PATCH 18/23] bugfix list command Forgot to update indexes after disabling the make:project command. --- tom/src/tom/application.cpp | 6 +++++- tom/src/tom/commands/listcommand.cpp | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tom/src/tom/application.cpp b/tom/src/tom/application.cpp index c8f5602c6..a3cdff7d8 100644 --- a/tom/src/tom/application.cpp +++ b/tom/src/tom/application.cpp @@ -62,6 +62,10 @@ 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, @@ -431,7 +435,7 @@ const std::vector &Application::commandNames() const // db "db:wipe", // make - "make:migration", "make:project", + "make:migration", /*"make:project",*/ // migrate "migrate:fresh", "migrate:install", "migrate:refresh", "migrate:reset", "migrate:rollback", "migrate:status", diff --git a/tom/src/tom/commands/listcommand.cpp b/tom/src/tom/commands/listcommand.cpp index a60afc87b..c806a72ab 100644 --- a/tom/src/tom/commands/listcommand.cpp +++ b/tom/src/tom/commands/listcommand.cpp @@ -207,14 +207,16 @@ ListCommand::getCommandsByNamespace(const QString &name) const std::vector> ListCommand::getCommandsInNamespace(const QString &name) const { - /* First number is index where it starts (0-based), second the number where it ends. + /* 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, 8)}, - {"migrate", std::make_tuple(8, 14)}, + {"make", std::make_tuple(6, 7)}, + {"migrate", std::make_tuple(7, 13)}, }; if (!cached.contains(name)) { From 436c2f7f8aa6b22af431ebe2c35deec59e81e58e Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 13:46:57 +0200 Subject: [PATCH 19/23] explicitly enable vt100 on MSYS2 ucrt64 The vt100 processing is not enabled on the MSYS2 ucrt64 by default so enable it explicitly. - also moved terminal initialization code to the Tom::Terminal class --- tom/include/tom/application.hpp | 5 ---- tom/include/tom/terminal.hpp | 13 +++++++++ tom/src/tom/application.cpp | 31 +++------------------ tom/src/tom/terminal.cpp | 48 +++++++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/tom/include/tom/application.hpp b/tom/include/tom/application.hpp index c95b07b11..196e67497 100644 --- a/tom/include/tom/application.hpp +++ b/tom/include/tom/application.hpp @@ -104,11 +104,6 @@ namespace Concerns /*! Alias for the commands' base class. */ using Command = Commands::Command; - /* Application initialization */ -#ifdef _WIN32 - /*! Prepare console input/output character encoding. */ - void initializeConsoleEncoding() const; -#endif /*! Fix m_argc/m_argv data members if the argv is empty. */ void fixEmptyArgv(); /*! Processes the specified function at application's normal exit. */ diff --git a/tom/include/tom/terminal.hpp b/tom/include/tom/terminal.hpp index 795c4b6eb..443ad7db3 100644 --- a/tom/include/tom/terminal.hpp +++ b/tom/include/tom/terminal.hpp @@ -31,6 +31,9 @@ namespace Tom /*! 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), @@ -75,6 +78,16 @@ namespace Tom 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. */ diff --git a/tom/src/tom/application.cpp b/tom/src/tom/application.cpp index a3cdff7d8..66f40c80e 100644 --- a/tom/src/tom/application.cpp +++ b/tom/src/tom/application.cpp @@ -3,13 +3,6 @@ #include #include -#ifdef _WIN32 -# include - -# include -# include -#endif - #include #include #include @@ -30,6 +23,7 @@ #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 @@ -81,10 +75,8 @@ Application::Application(int &argc, char **argv, std::shared_ptr + +# include +# include #elif defined(__linux__) # include #endif -#include - #include #include "tom/exceptions/invalidargumenterror.hpp" @@ -21,6 +22,17 @@ namespace Tom // 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); @@ -205,6 +217,38 @@ bool Terminal::hasVt100Support(std::wostream &wcout) const 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 From dec30fb972a783da500d6c99aaa2db1d974a962c Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 16:11:05 +0200 Subject: [PATCH 20/23] added linux g++/clang support --- tom/include/tom/terminal.hpp | 3 +++ tom/src/tom/commands/make/migrationcommand.cpp | 2 +- tom/src/tom/migrationcreator.cpp | 2 +- tom/src/tom/terminal.cpp | 7 +++++-- vcpkg.json | 3 ++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tom/include/tom/terminal.hpp b/tom/include/tom/terminal.hpp index 443ad7db3..22909494a 100644 --- a/tom/include/tom/terminal.hpp +++ b/tom/include/tom/terminal.hpp @@ -116,6 +116,9 @@ namespace Tom 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; diff --git a/tom/src/tom/commands/make/migrationcommand.cpp b/tom/src/tom/commands/make/migrationcommand.cpp index 17494d457..003902aaf 100644 --- a/tom/src/tom/commands/make/migrationcommand.cpp +++ b/tom/src/tom/commands/make/migrationcommand.cpp @@ -95,7 +95,7 @@ void MigrationCommand::writeMigration(const QString &name, const QString &table, info(QLatin1String("Created Migration: "), false); - note(QString::fromWCharArray(migrationFile.c_str())); + note(QString::fromStdString(migrationFile.string())); } fspath MigrationCommand::getMigrationPath() const diff --git a/tom/src/tom/migrationcreator.cpp b/tom/src/tom/migrationcreator.cpp index a45162cd7..d4c51bf53 100644 --- a/tom/src/tom/migrationcreator.cpp +++ b/tom/src/tom/migrationcreator.cpp @@ -75,7 +75,7 @@ void MigrationCreator::throwIfMigrationAlreadyExists(const QString &name, continue; // Extract migration name without datetime prefix and extension - auto entryName = QString::fromWCharArray(entry.path().filename().c_str()) + auto entryName = QString::fromStdString(entry.path().filename().string()) .mid(DatePrefix->size() + 1); entryName.truncate(entryName.lastIndexOf(DOT)); diff --git a/tom/src/tom/terminal.cpp b/tom/src/tom/terminal.cpp index 308a07b9c..0a82b0125 100644 --- a/tom/src/tom/terminal.cpp +++ b/tom/src/tom/terminal.cpp @@ -93,8 +93,11 @@ bool Terminal::hasWColorSupport(std::wostream &wcout) const bool Terminal::isatty(FILE *stream) const { - // CUR tom, linux silverqx - return _isatty(_fileno(stream)) != 0; +#ifdef _WIN32 + return ::_isatty(::_fileno(stream)) != 0; +#else + return ::isatty(::fileno(stream)) != 0; +#endif } int Terminal::width() 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": { From db45b4bd51673c581070da4005fbdf1a2a60eb77 Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 16:22:49 +0200 Subject: [PATCH 21/23] updated conf.pri.example --- conf.pri.example | 7 ++++++- tests/conf.pri.example | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) 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/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 From c41db5738aaa3c41f95f39c5f6e09cd9a162e99f Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 16:23:36 +0200 Subject: [PATCH 22/23] added tabulate dependency to vcpkg port --- cmake/vcpkg/ports/tinyorm/vcpkg.json | 1 + 1 file changed, 1 insertion(+) 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", From f483f2bee3addc5cc876b79b4359ddab8e96f4c1 Mon Sep 17 00:00:00 2001 From: silverqx Date: Thu, 21 Apr 2022 16:30:42 +0200 Subject: [PATCH 23/23] use $$shell_quote in QMAKE_CXXFLAGS --- docs/building.mdx | 4 ++-- docs/hello-world.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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