diff --git a/.gitignore b/.gitignore index 6a6daed4d..65fa4420d 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ Thumbs.db *.dll *.exe +# Test's Database +**/tests/**/testdata/vendor/ +**/tests/**/testdata/dotenv.ps1 diff --git a/NOTES.txt b/NOTES.txt index cb724f8db..4a0402f51 100644 --- a/NOTES.txt +++ b/NOTES.txt @@ -40,13 +40,14 @@ Common: - desirable : feature which is extremely wanted - dilemma : some sort of a fuckup - duplicate : duplicate code + - feature : some feature to implement, perpend before feature described below - future : task which has lower priority, because still much to do - mistake : bad decision during prototyping 😭 - move : c++ move semantics - mystery : don't know why that stuff is happening, find out what's up - now : do it before commit - next : next thing in the row to do after commit - - overflow : add check code, eg when size_t to int coversion + - overflow : add check code, eg when size_t to int conversion - perf : performance - production : check before deploy to production - reliability : make things more robust and reliable @@ -67,6 +68,7 @@ Features related/to implement: - primarykey dilema : different types for primary keys - relations : relations related 🤓 - scopes : query scopes + - table prefix : table prefix in the query grammar Versions info: @@ -75,6 +77,14 @@ Versions info: - based on Laravel v8.26.1 +Maintenance: +------------ + + - from time to time try: + - compile without PCH + - compile with Qt6, I have still problem with clazy + + constructor copy/move snippet: ------------------------------ @@ -143,14 +153,17 @@ DatabaseConnection config: QHash config { // {"driver", "mysql"}, // {"url", qEnvironmentVariable("DATABASE_URL")}, - {"host", qEnvironmentVariable("DB_HOST", "127.0.0.1")}, - {"port", qEnvironmentVariable("DB_PORT", "3306")}, - {"database", qEnvironmentVariable("DB_DATABASE", "")}, - {"username", qEnvironmentVariable("DB_USERNAME", "root")}, - {"password", qEnvironmentVariable("DB_PASSWORD", "")}, -// {"unix_socket", qEnvironmentVariable("DB_SOCKET", "")}, - {"charset", qEnvironmentVariable("DB_CHARSET", "utf8mb4")}, - {"collation", qEnvironmentVariable("DB_COLLATION", "utf8mb4_unicode_ci")}, +// {"url", qEnvironmentVariable("MYSQL_DATABASE_URL")}, + {"host", qEnvironmentVariable("DB_MYSQL_HOST", "127.0.0.1")}, + {"port", qEnvironmentVariable("DB_MYSQL_PORT", "3306")}, + {"database", qEnvironmentVariable("DB_MYSQL_DATABASE", "")}, + {"username", qEnvironmentVariable("DB_MYSQL_USERNAME", "root")}, + {"password", qEnvironmentVariable("DB_MYSQL_PASSWORD", "")}, +// {"unix_socket", qEnvironmentVariable("DB_MYSQL_SOCKET", "")}, + {"charset", qEnvironmentVariable("DB_MYSQL_CHARSET", "utf8mb4")}, + {"collation", qEnvironmentVariable("DB_MYSQL_COLLATION", "utf8mb4_unicode_ci")}, +// {"collation", qEnvironmentVariable("DB_MYSQL_COLLATION", "utf8mb4_0900_ai_ci")}, +// {"timezone", "+00:00"}, // {"prefix", ""}, // {"prefix_indexes", true}, {"strict", true}, @@ -158,6 +171,16 @@ QHash config { {"options", ""}, }; +QHash config { + {"driver", "QSQLITE"}, + {"database", qEnvironmentVariable("DB_SQLITE_DATABASE", "")}, + {"prefix", ""}, + {"options", QVariantHash()}, + {"foreign_key_constraints", qEnvironmentVariable("DB_SQLITE_FOREIGN_KEYS", + "true")}, + {"check_database_exists", true}, +}; + DatabaseConnection debug code: ------------------------------ diff --git a/TinyOrm.pro b/TinyOrm.pro index fe49c1b4e..80cd39659 100644 --- a/TinyOrm.pro +++ b/TinyOrm.pro @@ -1,7 +1,9 @@ TEMPLATE = subdirs -SUBDIRS += \ - src \ - tests \ +SUBDIRS += src -tests.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 +} diff --git a/docs/tinyorm.md b/docs/tinyorm.md index 1dbc81174..fb63fcb69 100644 --- a/docs/tinyorm.md +++ b/docs/tinyorm.md @@ -165,6 +165,10 @@ By default, all TinyOrm models will use the default database connection that is QString u_connection {"sqlite"}; }; +In special cases, when you want to query the database through a different connection, you can use `BaseModel::on` method, which takes the connection name as the first argument: + + auto user = User::on("sqlite")->find(1); + ## Retrieving Models diff --git a/include/include.pri b/include/include.pri index 73e8a36e5..d09179f4f 100644 --- a/include/include.pri +++ b/include/include.pri @@ -11,22 +11,27 @@ HEADERS += \ $$PWD/orm/connectors/connector.hpp \ $$PWD/orm/connectors/connectorinterface.hpp \ $$PWD/orm/connectors/mysqlconnector.hpp \ + $$PWD/orm/connectors/sqliteconnector.hpp \ $$PWD/orm/databaseconnection.hpp \ $$PWD/orm/databasemanager.hpp \ $$PWD/orm/db.hpp \ - $$PWD/orm/grammar.hpp \ $$PWD/orm/invalidformaterror.hpp \ $$PWD/orm/logquery.hpp \ $$PWD/orm/mysqlconnection.hpp \ $$PWD/orm/ormdomainerror.hpp \ + $$PWD/orm/orminvalidargumenterror.hpp \ $$PWD/orm/ormlogicerror.hpp \ $$PWD/orm/ormruntimeerror.hpp \ $$PWD/orm/ormtypes.hpp \ $$PWD/orm/query/expression.hpp \ + $$PWD/orm/query/grammars/grammar.hpp \ + $$PWD/orm/query/grammars/mysqlgrammar.hpp \ + $$PWD/orm/query/grammars/sqlitegrammar.hpp \ $$PWD/orm/query/joinclause.hpp \ $$PWD/orm/query/querybuilder.hpp \ $$PWD/orm/queryerror.hpp \ $$PWD/orm/sqlerror.hpp \ + $$PWD/orm/sqliteconnection.hpp \ $$PWD/orm/sqltransactionerror.hpp \ $$PWD/orm/support/configurationoptionsparser.hpp \ $$PWD/orm/tiny/basemodel.hpp \ diff --git a/include/orm/connectioninterface.hpp b/include/orm/connectioninterface.hpp index a7ebe0e35..b5154707f 100644 --- a/include/orm/connectioninterface.hpp +++ b/include/orm/connectioninterface.hpp @@ -13,7 +13,18 @@ namespace Orm { class DatabaseConnection; +namespace Query +{ + class Builder; + +namespace Grammars +{ class Grammar; +} +} + using QueryBuilder = Query::Builder; + using QueryGrammar = Query::Grammars::Grammar; + /*! Counts executed statements in a current connection. */ struct StatementsCounter { @@ -25,12 +36,6 @@ namespace Orm int transactional = -1; }; -namespace Query -{ - class Builder; -} - using QueryBuilder = Query::Builder; - class ConnectionInterface { public: @@ -116,7 +121,7 @@ namespace Query /*! Prepare the query bindings for execution. */ virtual QVector - prepareBindings(const QVector &bindings) const = 0; + prepareBindings(QVector bindings) const = 0; /*! Check database connection and show warnings when the state changed. */ virtual bool pingDatabase() = 0; @@ -130,8 +135,11 @@ namespace Query /*! Get the name of the connected database. */ virtual const QString &getDatabaseName() const = 0; + /*! Set the query grammar to the default implementation. */ + virtual void useDefaultQueryGrammar() = 0; + /*! Get the query grammar used by the connection. */ - virtual const Grammar &getQueryGrammar() const = 0; + virtual const QueryGrammar &getQueryGrammar() const = 0; /* Queries execution time counter */ /*! Determine whether we're counting queries execution time. */ @@ -160,6 +168,9 @@ namespace Query virtual StatementsCounter takeStatementsCounter() = 0; /*! Reset the number of executed queries. */ virtual DatabaseConnection &resetStatementsCounter() = 0; + + /*! Return the connection's driver name. */ + virtual QString driverName() = 0; }; } // namespace Orm diff --git a/include/orm/connectors/connector.hpp b/include/orm/connectors/connector.hpp index 3c17b6d44..4e4fa8ea4 100644 --- a/include/orm/connectors/connector.hpp +++ b/include/orm/connectors/connector.hpp @@ -31,7 +31,8 @@ namespace Orm::Connectors /*! Get the QSqlDatabase connection options based on the configuration. */ QString getOptions(const QVariantHash &config) const; - /*! Parse connection options. */ + /*! Parse and validate QSqlDatabase connection options, called from + the ConfigurationOptionsParser. */ virtual void parseConfigOptions(QVariantHash &options) const = 0; /*! Get the QSqlDatabase connection options for the current connector. */ virtual const QVariantHash &getConnectorOptions() const = 0; diff --git a/include/orm/connectors/mysqlconnector.hpp b/include/orm/connectors/mysqlconnector.hpp index 514a0c6f0..88ce571c3 100644 --- a/include/orm/connectors/mysqlconnector.hpp +++ b/include/orm/connectors/mysqlconnector.hpp @@ -19,7 +19,8 @@ namespace Orm::Connectors /*! Get the QSqlDatabase connection options for the current connector. */ const QVariantHash &getConnectorOptions() const override; - /*! Parse connection options. */ + /*! Parse and validate QSqlDatabase connection options, called from + the ConfigurationOptionsParser. */ void parseConfigOptions(QVariantHash &options) const override; protected: diff --git a/include/orm/connectors/sqliteconnector.hpp b/include/orm/connectors/sqliteconnector.hpp new file mode 100644 index 000000000..66f7c56b4 --- /dev/null +++ b/include/orm/connectors/sqliteconnector.hpp @@ -0,0 +1,44 @@ +#ifndef SQLITECONNECTOR_HPP +#define SQLITECONNECTOR_HPP + +#include "orm/connectors/connector.hpp" +#include "orm/connectors/connectorinterface.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Connectors +{ + + class SQLiteConnector final : public ConnectorInterface, public Connector + { + public: + /*! Establish a database connection. */ + ConnectionName connect(const QVariantHash &config) const override; + + /*! Get the QSqlDatabase connection options for the current connector. */ + const QVariantHash &getConnectorOptions() const override; + /*! Parse and validate QSqlDatabase connection options, called from + the ConfigurationOptionsParser. */ + void parseConfigOptions(QVariantHash &options) const override; + + protected: + /*! Set the connection foreign key constraints. */ + void configureForeignKeyConstraints(const QSqlDatabase &connection, + const QVariantHash &config) const; + + private: + /*! Check whether the SQLite database file exists. */ + void checkDatabaseExists(const QVariantHash &config) const; + + /*! The default QSqlDatabase connection options for the SQLiteConnector. */ + inline static const QVariantHash m_options {}; + }; + +} // namespace Orm::Connectors +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif + +#endif // SQLITECONNECTOR_HPP diff --git a/include/orm/databaseconnection.hpp b/include/orm/databaseconnection.hpp index 01b76a02b..85970ef93 100644 --- a/include/orm/databaseconnection.hpp +++ b/include/orm/databaseconnection.hpp @@ -10,7 +10,7 @@ #include "orm/concerns/detectslostconnections.hpp" #include "orm/connectioninterface.hpp" #include "orm/connectors/connectorinterface.hpp" -#include "orm/grammar.hpp" +#include "orm/query/grammars/grammar.hpp" #include "orm/queryerror.hpp" #ifdef TINYORM_COMMON_NAMESPACE @@ -112,7 +112,7 @@ namespace Orm /*! Prepare the query bindings for execution. */ QVector - prepareBindings(const QVector &bindings) const override; + prepareBindings(QVector bindings) const override; /*! Bind values to their parameters in the given statement. */ void bindValues(QSqlQuery &query, const QVector &bindings) const; @@ -128,15 +128,21 @@ namespace Orm /*! Disconnect from the underlying PDO connection. */ void disconnect(); + /*! Get the default query grammar instance. */ + virtual std::unique_ptr getDefaultQueryGrammar() const = 0; + /*! Get the database connection name. */ inline const QString getName() const override { return getConfig("name").toString(); } /*! Get the name of the connected database. */ inline const QString &getDatabaseName() const override { return m_database; } + /*! Set the query grammar to the default implementation. */ + inline void useDefaultQueryGrammar() override + { m_queryGrammar = getDefaultQueryGrammar(); } /*! Get the query grammar used by the connection. */ - inline const Grammar &getQueryGrammar() const override - { return m_queryGrammar; } + inline const QueryGrammar &getQueryGrammar() const override + { return *m_queryGrammar; } // TODO duplicate, extract to some internal types silverqx /*! Reconnector lambda type. */ @@ -147,7 +153,7 @@ namespace Orm /*! Get an option from the configuration options. */ QVariant getConfig(const QString &option) const; /*! Get the configuration for the current connection. */ - QVariant getConfig() const; + const QVariantHash &getConfig() const; /* Queries execution time counter */ /*! Determine whether we're counting queries execution time. */ @@ -177,6 +183,9 @@ namespace Orm /*! Reset the number of executed queries. */ DatabaseConnection &resetStatementsCounter() override; + /*! Return the connection's driver name. */ + QString driverName() override; + protected: /*! Callback type used in the run() methods. */ template @@ -201,12 +210,18 @@ namespace Orm /*! Reset in transaction state and savepoints. */ DatabaseConnection &resetTransactions(); + /*! Log database disconnected, examined during MySQL ping. */ + void logDisconnected(); + /*! Log database connected, examined during MySQL ping. */ + void logConnected(); + /*! The active QSqlDatabase connection name. */ std::optional m_qtConnection; /*! The QSqlDatabase connection resolver. */ std::function m_qtConnectionResolver; /*! The name of the connected database. */ const QString m_database; + // TODO feature, table prefix silverqx /*! The table prefix for the connection. */ const QString m_tablePrefix {""}; /*! The database connection configuration options. */ @@ -247,10 +262,6 @@ namespace Orm const QVector &bindings, const RunCallback &callback) const; - /*! Log database disconnected, examined during MySQL ping. */ - void logDisconnected(); - /*! Log database connected, examined during MySQL ping. */ - void logConnected(); /*! Log a transaction query into the connection's query log. */ void logTransactionQuery(const QString &query, const std::optional &elapsed); @@ -267,7 +278,7 @@ namespace Orm /*! Active savepoints counter. */ uint m_savepoints = 0; /*! The query grammar implementation. */ - Grammar m_queryGrammar; + std::unique_ptr m_queryGrammar; #ifdef TINYORM_DEBUG_SQL /*! Indicates whether logging of sql queries is enabled. */ @@ -276,7 +287,6 @@ namespace Orm /*! Indicates whether logging of sql queries is enabled. */ const bool m_debugSql = false; #endif - }; template diff --git a/include/orm/databasemanager.hpp b/include/orm/databasemanager.hpp index eed0dc8d5..4a838f5bf 100644 --- a/include/orm/databasemanager.hpp +++ b/include/orm/databasemanager.hpp @@ -111,6 +111,8 @@ namespace Query QStringList supportedDrivers() const; /*! Returns a list containing the names of all connections. */ QStringList connectionNames() const; + /*! Returns a list containing the names of opened connections. */ + QStringList openedConnectionNames() const; /*! Get the default connection name. */ const QString &getDefaultConnection() const override; @@ -150,6 +152,17 @@ namespace Query /*! Reset queries execution time on all active connections. */ void resetAllElapsedCounters(); + /*! Enable counting queries execution time on given connections. */ + inline void enableElapsedCounters(const QStringList &connections); + /*! Disable counting queries execution time on given connections. */ + inline void disableElapsedCounters(const QStringList &connections); + /*! Obtain queries execution time from given connections. */ + inline qint64 getElapsedCounters(const QStringList &connections); + /*! Obtain and reset queries execution time on given connections. */ + inline qint64 takeElapsedCounters(const QStringList &connections); + /*! Reset queries execution time on given connections. */ + inline void resetElapsedCounters(const QStringList &connections); + /* Queries executed counter */ /*! Determine whether we're counting the number of executed queries. */ bool countingStatements(const QString &connection = ""); @@ -183,6 +196,17 @@ namespace Query /*! Reset the number of executed queries on all active connections. */ void resetAllStatementCounters(); + /*! Enable counting the number of executed queries on given connections. */ + inline void enableStatementCounters(const QStringList &connections); + /*! Disable counting the number of executed queries on given connections. */ + inline void disableStatementCounters(const QStringList &connections); + /*! Obtain the number of executed queries on given connections. */ + inline StatementsCounter getStatementCounters(const QStringList &connections); + /*! Obtain and reset the number of executed queries on given connections. */ + inline StatementsCounter takeStatementCounters(const QStringList &connections); + /*! Reset the number of executed queries on given connections. */ + inline void resetStatementCounters(const QStringList &connections); + protected: /*! Default connection name. */ static const char *defaultConnectionName; diff --git a/include/orm/db.hpp b/include/orm/db.hpp index e84442c77..891682017 100644 --- a/include/orm/db.hpp +++ b/include/orm/db.hpp @@ -58,6 +58,8 @@ namespace Orm static const QStringList supportedDrivers(); /*! Returns a list containing the names of all connections. */ static QStringList connectionNames(); + /*! Returns a list containing the names of opened connections. */ + static QStringList openedConnectionNames(); /*! Get the default connection name. */ static const QString &getDefaultConnection(); @@ -155,6 +157,17 @@ namespace Orm /*! Reset queries execution time on all active connections. */ static void resetAllElapsedCounters(); + /*! Enable counting queries execution time on given connections. */ + static void enableElapsedCounters(const QStringList &connections); + /*! Disable counting queries execution time on given connections. */ + static void disableElapsedCounters(const QStringList &connections); + /*! Obtain queries execution time from given connections. */ + static qint64 getElapsedCounters(const QStringList &connections); + /*! Obtain and reset queries execution time on given connections. */ + static qint64 takeElapsedCounters(const QStringList &connections); + /*! Reset queries execution time on given connections. */ + static void resetElapsedCounters(const QStringList &connections); + /* Queries executed counter */ /*! Determine whether we're counting the number of executed queries. */ static bool @@ -188,6 +201,17 @@ namespace Orm static StatementsCounter takeAllStatementCounters(); /*! Reset the number of executed queries on all active connections. */ static void resetAllStatementCounters(); + + /*! Enable counting the number of executed queries on given connections. */ + static void enableStatementCounters(const QStringList &connections); + /*! Disable counting the number of executed queries on given connections. */ + static void disableStatementCounters(const QStringList &connections); + /*! Obtain the number of executed queries on given connections. */ + static StatementsCounter getStatementCounters(const QStringList &connections); + /*! Obtain and reset the number of executed queries on given connections. */ + static StatementsCounter takeStatementCounters(const QStringList &connections); + /*! Reset the number of executed queries on given connections. */ + static void resetStatementCounters(const QStringList &connections); }; } // namespace Orm diff --git a/include/orm/mysqlconnection.hpp b/include/orm/mysqlconnection.hpp index 420bd141d..40fe13ab6 100644 --- a/include/orm/mysqlconnection.hpp +++ b/include/orm/mysqlconnection.hpp @@ -18,6 +18,15 @@ namespace Orm const QString &database = "", const QString tablePrefix = "", const QVariantHash &config = {}); inline virtual ~MySqlConnection() = default; + + /*! Get the default query grammar instance. */ + std::unique_ptr getDefaultQueryGrammar() const override; + + /*! Determine if the connected database is a MariaDB database. */ + inline bool isMaria(); + + /*! Check database connection and show warnings when the state changed. */ + bool pingDatabase() override; }; } // namespace Orm diff --git a/include/orm/orminvalidargumenterror.hpp b/include/orm/orminvalidargumenterror.hpp new file mode 100644 index 000000000..49c907064 --- /dev/null +++ b/include/orm/orminvalidargumenterror.hpp @@ -0,0 +1,26 @@ +#ifndef ORMINVALIDARGUMENTERROR_H +#define ORMINVALIDARGUMENTERROR_H + +#include + +#include "export.hpp" +#include "orm/ormlogicerror.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm +{ + + class SHAREDLIB_EXPORT OrmInvalidArgumentError : public OrmLogicError + { + using OrmLogicError::OrmLogicError; + }; + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif + +#endif // ORMINVALIDARGUMENTERROR_H diff --git a/include/orm/grammar.hpp b/include/orm/query/grammars/grammar.hpp similarity index 80% rename from include/orm/grammar.hpp rename to include/orm/query/grammars/grammar.hpp index eea43ecc1..415f9e14a 100644 --- a/include/orm/grammar.hpp +++ b/include/orm/query/grammars/grammar.hpp @@ -8,45 +8,76 @@ namespace TINYORM_COMMON_NAMESPACE { #endif -namespace Orm +namespace Orm::Query::Grammars { class SHAREDLIB_EXPORT Grammar { public: + virtual ~Grammar() = default; + /*! Compile a select query into SQL. */ QString compileSelect(QueryBuilder &query) const; + /*! Compile an insert statement into SQL. */ - QString compileInsert(const QueryBuilder &query, - const QVector &values) const; + virtual QString + compileInsert(const QueryBuilder &query, + const QVector &values) const; /*! Compile an insert ignore statement into SQL. */ - QString compileInsertOrIgnore(const QueryBuilder &query, - const QVector &values) const; + virtual QString + compileInsertOrIgnore(const QueryBuilder &query, + const QVector &values) const; // TODO postgres, sequence silverqx /*! Compile an insert and get ID statement into SQL. */ inline QString compileInsertGetId(const QueryBuilder &query, const QVector &values) const { return compileInsert(query, values); } + /*! Compile an update statement into SQL. */ - QString compileUpdate(const QueryBuilder &query, - const QVector &values) const; + virtual QString + compileUpdate(QueryBuilder &query, const QVector &values) const; /*! Prepare the bindings for an update statement. */ QVector prepareBindingsForUpdate(const BindingsMap &bindings, const QVector &values) const; + /*! Compile a delete statement into SQL. */ - QString compileDelete(const QueryBuilder &query) const; + virtual QString compileDelete(QueryBuilder &query) const; /*! Prepare the bindings for a delete statement. */ QVector prepareBindingsForDelete(const BindingsMap &bindings) const; - /*! Compile a truncate table statement into SQL. */ - QString compileTruncate(const QueryBuilder &query) const; + + /*! Compile a truncate table statement into SQL. Returns a map of + the query string and bindings. */ + virtual std::unordered_map> + compileTruncate(const QueryBuilder &query) const; /*! Get the format for database stored dates. */ const QString &getDateFormat() const; + /*! Get the grammar specific operators. */ + virtual const QVector &getOperators() const; + protected: + /*! Compile the "order by" portions of the query. */ + QString compileOrders(const QueryBuilder &query) const; + /*! Compile the "limit" portions of the query. */ + QString compileLimit(const QueryBuilder &query) const; + + /*! Compile the columns for an update statement. */ + virtual QString + compileUpdateColumns(const QVector &values) const; + /*! Compile an update statement without joins into SQL. */ + virtual QString + compileUpdateWithoutJoins(const QueryBuilder &query, const QString &table, + const QString &columns, const QString &wheres) const; + + /*! Compile a delete statement without joins into SQL. */ + virtual QString + compileDeleteWithoutJoins(const QueryBuilder &query, const QString &table, + const QString &wheres) const; + // TODO methods below should be abstracted to DatabaseGrammar silverqx /*! Convert an array of column names into a delimited string. */ QString columnize(const QStringList &columns) const; @@ -62,6 +93,11 @@ namespace Orm /*! Remove the leading boolean from a statement. */ QString removeLeadingBoolean(QString statement) const; + /*! Get an alias from the 'from' clause. */ + QString getAliasFromFrom(const QString &from) const; + /*! Get the column name without the table name, a string after last dot. */ + QString unqualifyColumn(const QString &column) const; + private: /*! The components necessary for a select clause. */ using SelectComponentsVector = QVector; @@ -101,8 +137,7 @@ namespace Orm /*! Compile the "where" portions of the query. */ QString compileWheres(const QueryBuilder &query) const; /*! Get the vector of all the where clauses for the query. */ - QVector - compileWheresToVector(const QueryBuilder &query) const; + QVector compileWheresToVector(const QueryBuilder &query) const; /*! Format the where clause statements into one string. */ QString concatenateWhereClauses(const QueryBuilder &query, const QVector &sql) const; @@ -135,35 +170,20 @@ namespace Orm /*! Compile a "where not null" clause. */ QString whereNotNull(const WhereConditionItem &where) const; - /*! Compile the "order by" portions of the query. */ - QString compileOrders(const QueryBuilder &query) const; /*! Compile the query orders to the vector. */ QVector compileOrdersToVector(const QueryBuilder &query) const; - /*! Compile the "limit" portions of the query. */ - QString compileLimit(const QueryBuilder &query) const; /*! Compile the "offset" portions of the query. */ QString compileOffset(const QueryBuilder &query) const; /*! Compile a insert values lists. */ - QVector compileInsertToVector(const QVector &values) const; - /*! Compile an insert statement into SQL. */ - QString - compileInsert(const QueryBuilder &query, const QVector &values, - bool ignore) const; + QVector + compileInsertToVector(const QVector &values) const; - /*! Compile the columns for an update statement. */ - QString compileUpdateColumns(const QVector &values) const; - /*! Compile an update statement without joins into SQL. */ - QString compileUpdateWithoutJoins(const QString &table, const QString &columns, - const QString &wheres) const; /*! Compile an update statement with joins into SQL. */ QString compileUpdateWithJoins(const QueryBuilder &query, const QString &table, const QString &columns, const QString &wheres) const; - /*! Compile a delete statement without joins into SQL. */ - QString compileDeleteWithoutJoins(const QString &table, - const QString &wheres) const; /*! Compile a delete statement with joins into SQL. */ QString compileDeleteWithJoins(const QueryBuilder &query, const QString &table, const QString &wheres) const; diff --git a/include/orm/query/grammars/mysqlgrammar.hpp b/include/orm/query/grammars/mysqlgrammar.hpp new file mode 100644 index 000000000..c646c4ce9 --- /dev/null +++ b/include/orm/query/grammars/mysqlgrammar.hpp @@ -0,0 +1,44 @@ +#ifndef MYSQLGRAMMAR_H +#define MYSQLGRAMMAR_H + +#include "orm/query/grammars/grammar.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Query::Grammars +{ + + class SHAREDLIB_EXPORT MySqlGrammar : public Grammar + { + public: + /*! Compile an insert statement into SQL. */ + QString compileInsert(const QueryBuilder &query, + const QVector &values) const override; + /*! Compile an insert ignore statement into SQL. */ + QString compileInsertOrIgnore(const QueryBuilder &query, + const QVector &values) const override; + + /*! Get the grammar specific operators. */ + const QVector &getOperators() const override; + + protected: + /*! Compile an update statement without joins into SQL. */ + QString + compileUpdateWithoutJoins( + const QueryBuilder &query, const QString &table, + const QString &columns, const QString &wheres) const override; + + /*! Compile a delete statement without joins into SQL. */ + QString + compileDeleteWithoutJoins(const QueryBuilder &query, const QString &table, + const QString &wheres) const override; + }; + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif + +#endif // MYSQLGRAMMAR_H diff --git a/include/orm/query/grammars/sqlitegrammar.hpp b/include/orm/query/grammars/sqlitegrammar.hpp new file mode 100644 index 000000000..5b1d0fa0c --- /dev/null +++ b/include/orm/query/grammars/sqlitegrammar.hpp @@ -0,0 +1,57 @@ +#ifndef SQLITEGRAMMAR_H +#define SQLITEGRAMMAR_H + +#include "orm/query/grammars/grammar.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Query::Grammars +{ + + class SHAREDLIB_EXPORT SQLiteGrammar : public Grammar + { + + public: + /*! Compile an insert ignore statement into SQL. */ + QString compileInsertOrIgnore(const QueryBuilder &query, + const QVector &values) const override; + + /*! Compile an update statement into SQL. */ + QString compileUpdate(QueryBuilder &query, + const QVector &values) const override; + + /*! Compile a delete statement into SQL. */ + QString compileDelete(QueryBuilder &query) const override; + + /*! Compile a truncate table statement into SQL. Returns a map of + the query string and bindings. */ + std::unordered_map> + compileTruncate(const QueryBuilder &query) const override; + + /*! Get the grammar specific operators. */ + const QVector &getOperators() const override; + + protected: + /*! Compile the columns for an update statement. */ + QString compileUpdateColumns(const QVector &values) const override; + + private: + /*! Compile an update statement with joins or limit into SQL. */ + QString compileUpdateWithJoinsOrLimit(QueryBuilder &query, + const QVector &values) const; + + /*! Compile a delete statement with joins or limit into SQL. */ + QString compileDeleteWithJoinsOrLimit(QueryBuilder &query) const; + + /*! The grammar specific operators. */ + const QVector m_operators {"sounds like"}; + }; + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif + +#endif // SQLITEGRAMMAR_H diff --git a/include/orm/query/querybuilder.hpp b/include/orm/query/querybuilder.hpp index 76714b478..dcfa45c10 100644 --- a/include/orm/query/querybuilder.hpp +++ b/include/orm/query/querybuilder.hpp @@ -16,20 +16,24 @@ namespace TINYORM_COMMON_NAMESPACE namespace Orm { class ConnectionInterface; - class Grammar; } namespace Orm::Query { - class JoinClause; +namespace Grammars +{ + class Grammar; +} + using QueryGrammar = Query::Grammars::Grammar; + // TODO add support for subqueries, first in where() silverqx // TODO add inRandomOrder() silverqx class SHAREDLIB_EXPORT Builder { public: - Builder(ConnectionInterface &connection, const Grammar &grammar); + Builder(ConnectionInterface &connection, const QueryGrammar &grammar); // WARNING solve pure virtual dtor vs default silverqx /* Need to be the polymorphic type because of dynamic_cast<> in Grammar::concatenateWhereClauses(). */ @@ -86,7 +90,7 @@ namespace Orm::Query std::tuple remove(const quint64 id); /*! Run a truncate statement on the table. */ - std::tuple truncate(); + void truncate(); /* Select */ /*! Set the columns to be selected. */ @@ -266,7 +270,7 @@ namespace Orm::Query inline ConnectionInterface &getConnection() const { return m_connection; } /*! Get the query grammar instance. */ - inline const Grammar &getGrammar() const + inline const QueryGrammar &getGrammar() const { return m_grammar; } /*! Get the current query value bindings as flattened QVector. */ @@ -375,7 +379,7 @@ namespace Orm::Query /*! The database connection instance. */ ConnectionInterface &m_connection; /*! The database query grammar instance. */ - const Grammar &m_grammar; + const QueryGrammar &m_grammar; /*! The current query value bindings. Order is crucial here because of that QMap with an enum struct is used. */ @@ -407,6 +411,7 @@ namespace Orm::Query QVector m_havings; /*! The orderings for the query. */ QVector m_orders; + // BUG I think that limit can be also negative silverqx /*! The maximum number of records to return. */ int m_limit = -1; /*! The number of records to skip. */ diff --git a/include/orm/sqliteconnection.hpp b/include/orm/sqliteconnection.hpp new file mode 100644 index 000000000..ff5437f34 --- /dev/null +++ b/include/orm/sqliteconnection.hpp @@ -0,0 +1,31 @@ +#ifndef SQLITECONNECTION_HPP +#define SQLITECONNECTION_HPP + +#include "orm/databaseconnection.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm +{ + + class SHAREDLIB_EXPORT SQLiteConnection final : public DatabaseConnection + { + public: + SQLiteConnection( + const std::function &connection, + const QString &database = "", const QString tablePrefix = "", + const QVariantHash &config = {}); + inline virtual ~SQLiteConnection() = default; + + /*! Get the default query grammar instance. */ + std::unique_ptr getDefaultQueryGrammar() const override; + }; + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif + +#endif // SQLITECONNECTION_HPP diff --git a/include/orm/support/configurationoptionsparser.hpp b/include/orm/support/configurationoptionsparser.hpp index 0ef5b1aa4..3d124e3d2 100644 --- a/include/orm/support/configurationoptionsparser.hpp +++ b/include/orm/support/configurationoptionsparser.hpp @@ -19,19 +19,25 @@ namespace Connectors namespace Support { + /*! Validate, prepare, and merge QSqlDatabase connection options, these are + the settings passed to the QSqlDatabase::setConnectOptions(). */ class ConfigurationOptionsParser { public: ConfigurationOptionsParser(const Connectors::Connector &connector); - /*! Parse the database configuration, validate, prepare, and merge connection options. */ + /*! Parse the database configuration, validate, prepare, and merge connection + options. */ QString parseConfiguration(const QVariantHash &config) const; protected: - /*! Validate the 'options' configuration type, has to be QString or QVariantHash. */ + /*! Validate the 'options' configuration type, has to be the QString or + QVariantHash. */ void validateConfigOptions(const QVariant &options) const; - /*! Prepare options for parseConfigOptions() function, convert to the QVariantHash if needed. */ + /*! Prepare options for parseConfigOptions() function, convert to + the QVariantHash if needed. */ QVariantHash prepareConfigOptions(const QVariant &options) const; - /*! Merge the TinyORM connector options with user's provided connection options defined in the config. */ + /*! Merge the TinyORM connector options with user's provided connection + options defined in the config. */ QVariantHash mergeOptions(const QVariantHash &connectortOptions, const QVariantHash &preparedConfigOptions) const; /*! Stringify merged options. */ diff --git a/include/orm/tiny/basemodel.hpp b/include/orm/tiny/basemodel.hpp index 1793a7a94..adbedbf75 100644 --- a/include/orm/tiny/basemodel.hpp +++ b/include/orm/tiny/basemodel.hpp @@ -39,6 +39,16 @@ namespace Relations { using QueryBuilder = Query::Builder; +#ifdef TINYORM_TESTS_CODE + /*! Used by tests to override connection in the BaseModel. */ + struct ConnectionOverride + { + /*! The connection to use in the BaseModel, this data member is picked up + in the BaseModel::getConnectionName(). */ + inline static QString connection = ""; + }; +#endif + // TODO decide/unify when to use class/typename keywords for templates silverqx // TODO concept, AllRelations can not contain type defined in "Model" parameter silverqx // TODO next test no relation behavior silverqx @@ -86,6 +96,8 @@ namespace Relations { /* Static operations on a model instance */ /*! Begin querying the model. */ static std::unique_ptr> query(); + /*! Begin querying the model on a given connection. */ + static std::unique_ptr> on(const QString &connection = ""); /* TinyBuilder proxy methods */ /*! Get all of the models from the database. */ @@ -164,7 +176,7 @@ namespace Relations { static std::size_t destroy(QVariant id); /*! Run a truncate statement on the table. */ - static std::tuple truncate(); + static void truncate(); /* Select */ /*! Set the columns to be selected. */ @@ -469,8 +481,7 @@ namespace Relations { /* Getters / Setters */ /*! Get the current connection name for the model. */ - inline const QString &getConnectionName() const - { return model().u_connection; } + const QString &getConnectionName() const; /*! Get the database connection for the model. */ inline ConnectionInterface &getConnection() const { return m_resolver->connection(getConnectionName()); } @@ -856,7 +867,9 @@ namespace Relations { // TODO future Default Attribute Values, can not be u_attributes because of CRTP, because BaseModel is initialized first and u_attributes are uninitialized, the best I've come up with was BaseModel.init() and init default attrs. from there silverqx /*! The model's attributes. */ QVector m_attributes; - /*! The model attribute's original state. */ + /*! The model attribute's original state. + On the model from many-to-many relation also contains all pivot values, + that is normal. */ QVector m_original; /*! The changed model attributes. */ QVector m_changes; @@ -1020,6 +1033,21 @@ namespace Relations { return Model().newQuery(); } + template + std::unique_ptr> + BaseModel::on(const QString &connection) + { + /* First we will just create a fresh instance of this model, and then we can + set the connection on the model so that it is used for the queries we + execute, as well as being set on every relation we retrieve without + a custom connection name. */ + Model instance; + + instance.setConnection(connection); + + return instance.newQuery(); + } + template QVector BaseModel::all(const QStringList &columns) @@ -1208,10 +1236,9 @@ namespace Relations { } template - std::tuple - BaseModel::truncate() + void BaseModel::truncate() { - return query()->truncate(); + query()->truncate(); } template @@ -2489,6 +2516,21 @@ namespace Relations { parent, attributes, table, exists); } + template + const QString & + BaseModel::getConnectionName() const + { +#ifdef TINYORM_TESTS_CODE + // Used from tests to override connection + if (const auto &connection = ConnectionOverride::connection; + !connection.isEmpty() + ) + return connection; +#endif + + return model().u_connection; + } + template QString BaseModel::getTable() const { diff --git a/include/orm/tiny/relations/relation.hpp b/include/orm/tiny/relations/relation.hpp index d6ffd1157..d2e66c40b 100644 --- a/include/orm/tiny/relations/relation.hpp +++ b/include/orm/tiny/relations/relation.hpp @@ -169,7 +169,7 @@ namespace Relations std::tuple deleteModels() const; /*! Run a truncate statement on the table. */ - std::tuple truncate() const; + void truncate() const; /* Select */ /*! Set the columns to be selected. */ @@ -709,10 +709,9 @@ namespace Relations } template - std::tuple - Relation::truncate() const + void Relation::truncate() const { - return m_query->truncate(); + m_query->truncate(); } template diff --git a/include/orm/tiny/tinybuilder.hpp b/include/orm/tiny/tinybuilder.hpp index 4a0ff614b..09d9aae2c 100644 --- a/include/orm/tiny/tinybuilder.hpp +++ b/include/orm/tiny/tinybuilder.hpp @@ -120,7 +120,7 @@ namespace Relations std::tuple deleteModels(); /*! Run a truncate statement on the table. */ - std::tuple truncate(); + void truncate(); /* Select */ /*! Set the columns to be selected. */ @@ -627,9 +627,9 @@ namespace Relations } template - std::tuple Builder::truncate() + void Builder::truncate() { - return toBase().truncate(); + toBase().truncate(); } template diff --git a/src/orm/connectors/connectionfactory.cpp b/src/orm/connectors/connectionfactory.cpp index 3afa84425..79ea84184 100644 --- a/src/orm/connectors/connectionfactory.cpp +++ b/src/orm/connectors/connectionfactory.cpp @@ -1,7 +1,9 @@ #include "orm/connectors/connectionfactory.hpp" #include "orm/connectors/mysqlconnector.hpp" +#include "orm/connectors/sqliteconnector.hpp" #include "orm/mysqlconnection.hpp" +#include "orm/sqliteconnection.hpp" #ifdef TINYORM_COMMON_NAMESPACE namespace TINYORM_COMMON_NAMESPACE @@ -34,8 +36,8 @@ ConnectionFactory::createConnector(const QVariantHash &config) const return std::make_unique(); // else if (driver == "QPSQL") // return std::make_unique(); -// else if (driver == "QSQLITE") -// return std::make_unique(); + else if (driver == "QSQLITE") + return std::make_unique(); // else if (driver == "SQLSRV") // return std::make_unique(); else @@ -124,14 +126,13 @@ ConnectionFactory::createQSqlDatabaseResolverWithHosts(const QVariantHash &confi } std::function -ConnectionFactory::createQSqlDatabaseResolverWithoutHosts(const QVariantHash &) const +ConnectionFactory::createQSqlDatabaseResolverWithoutHosts( + const QVariantHash &config) const { - throw std::domain_error( - "createQSqlDatabaseResolverWithoutHosts() not yet implemented."); - -// return function () use ($config) { -// return $this->createConnector($config)->connect($config); -// }; + return [this, &config]() -> ConnectionName + { + return createConnector(config)->connect(config); + }; } std::unique_ptr @@ -145,8 +146,8 @@ ConnectionFactory::createConnection( return std::make_unique(connection, database, prefix, config); // else if (driver == "QPSQL") // return std::make_unique(connection, database, prefix, config); -// else if (driver == "QSQLITE") -// return std::make_unique(connection, database, prefix, config); + else if (driver == "QSQLITE") + return std::make_unique(connection, database, prefix, config); // else if (driver == "SQLSRV") // return std::make_unique(connection, database, prefix, config); else @@ -157,6 +158,7 @@ ConnectionFactory::createConnection( QStringList ConnectionFactory::parseHosts(const QVariantHash &config) const { if (!config.contains("host")) + // TODO now unify exception, std::domain_error is for user code, create own exceptions and use InvalidArgumentError, or runtime/logic error silverqx throw std::domain_error("Database 'host' configuration parameter is required."); const auto hosts = config["host"].value(); diff --git a/src/orm/connectors/connector.cpp b/src/orm/connectors/connector.cpp index f3ff191ec..6f1f88756 100644 --- a/src/orm/connectors/connector.cpp +++ b/src/orm/connectors/connector.cpp @@ -48,7 +48,10 @@ QString Connector::getOptions(const QVariantHash &config) const any default connection options which are common for all drivers, instead every driver has it's own connection options. So I have divided it into two options, one are config options which are - defined by the user and others are connector options. */ + defined by the user and others are connector options. + Options defined by a user are in the config's 'options' parameter and + connector options are defined in the connector itself as 'm_options' + data member. */ // Validate, prepare, and merge connection options return Support::ConfigurationOptionsParser(*this) .parseConfiguration(config); @@ -60,18 +63,19 @@ Connector::addQSqlDatabaseConnection(const QString &name, const QVariantHash &co { QSqlDatabase db; - db = QSqlDatabase::addDatabase(config["driver"].toString(), name); + // TODO now change all QVariant conversions to value<>() silverqx + db = QSqlDatabase::addDatabase(config["driver"].value(), name); - db.setHostName(config["host"].toString()); + db.setHostName(config["host"].value()); if (config.contains("database")) - db.setDatabaseName(config["database"].toString()); + db.setDatabaseName(config["database"].value()); if (config.contains("username")) - db.setUserName(config["username"].toString()); + db.setUserName(config["username"].value()); if (config.contains("password")) - db.setPassword(config["password"].toString()); + db.setPassword(config["password"].value()); if (config.contains("port")) - db.setPort(config["port"].toUInt()); + db.setPort(config["port"].value()); db.setConnectOptions(options); diff --git a/src/orm/connectors/mysqlconnector.cpp b/src/orm/connectors/mysqlconnector.cpp index 30094dec7..e25c967a6 100644 --- a/src/orm/connectors/mysqlconnector.cpp +++ b/src/orm/connectors/mysqlconnector.cpp @@ -3,8 +3,6 @@ #include #include -#include - #ifdef TINYORM_COMMON_NAMESPACE namespace TINYORM_COMMON_NAMESPACE { @@ -19,7 +17,7 @@ MySqlConnector::connect(const QVariantHash &config) const /* We need to grab the QSqlDatabse options that should be used while making the brand new connection instance. The QSqlDatabase options control various - aspects of the connection's behavior, and can be override by the developers. */ + aspects of the connection's behavior, and can be overridden by the developers. */ const auto options = getOptions(config); // Create and open new database connection @@ -142,8 +140,9 @@ void MySqlConnector::setModes(const QSqlDatabase &connection, } } else { - QSqlQuery query(QStringLiteral("set session sql_mode='NO_ENGINE_SUBSTITUTION'"), - connection); + QSqlQuery query( + QStringLiteral("set session sql_mode='NO_ENGINE_SUBSTITUTION'"), + connection); query.exec(); } } diff --git a/src/orm/connectors/sqliteconnector.cpp b/src/orm/connectors/sqliteconnector.cpp new file mode 100644 index 000000000..d971fac14 --- /dev/null +++ b/src/orm/connectors/sqliteconnector.cpp @@ -0,0 +1,94 @@ +#include "orm/connectors/sqliteconnector.hpp" + +#include + +#include "orm/orminvalidargumenterror.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Connectors +{ + +ConnectionName +SQLiteConnector::connect(const QVariantHash &config) const +{ + const auto name = config["name"].toString(); + + const auto options = getOptions(config); + + /* SQLite supports "in-memory" databases that only last as long as the owning + connection does. These are useful for tests or for short lifetime store + querying. In-memory databases may only have a single open connection. */ + if (config["database"] == ":memory:") { + // sqlite :memory: driver + createConnection(name, config, options); + + return name; + } + + /* Here we'll verify that the SQLite database exists before going any further + as the developer probably wants to know if the database exists and this + SQLite driver will not throw any exception if it does not by default. */ + checkDatabaseExists(config); + + // Create and open new database connection + const auto connection = createConnection(name, config, options); + + // Foreign key constraints + configureForeignKeyConstraints(connection, config); + + /* Return only connection name, because QSqlDatabase documentation doesn't + recommend to store QSqlDatabase instance as a class data member, we can + simply obtain the connection by QSqlDatabase::connection() when needed. */ + return name; +} + +const QVariantHash & +SQLiteConnector::getConnectorOptions() const +{ + return m_options; +} + +void SQLiteConnector::parseConfigOptions(QVariantHash &) const +{} + +void SQLiteConnector::configureForeignKeyConstraints( + const QSqlDatabase &connection, const QVariantHash &config) const +{ + // This ensures default SQLite behavior + if (!config.contains("foreign_key_constraints")) + return; + + const auto foreignKeyConstraints = + config["foreign_key_constraints"].value() ? "ON" : "OFF"; + + QSqlQuery query(connection); + // TODO feature, schema builder, foreign key constraints silverqx + query.prepare(QStringLiteral("PRAGMA foreign_keys = ?;")); + query.addBindValue(foreignKeyConstraints); + + query.exec(); +} + +void SQLiteConnector::checkDatabaseExists(const QVariantHash &config) const +{ + const auto path = config["database"].value(); + + // Default behavior is to check database existence + bool checkDatabaseExists = true; + if (const auto &configCheckDatabase = config["check_database_exists"]; + configCheckDatabase.isValid() && !configCheckDatabase.isNull() + ) + checkDatabaseExists = config["check_database_exists"].value(); + + if (checkDatabaseExists && !QFile::exists(path)) + throw OrmInvalidArgumentError( + QStringLiteral("SQLite Database file '%1' does not exist.").arg(path)); +} + +} // namespace Orm::Connectors +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif diff --git a/src/orm/databaseconnection.cpp b/src/orm/databaseconnection.cpp index 180edc525..d2bcb0c77 100644 --- a/src/orm/databaseconnection.cpp +++ b/src/orm/databaseconnection.cpp @@ -1,11 +1,7 @@ #include "orm/databaseconnection.hpp" -#include -#include - -#include "mysql.h" - #include "orm/logquery.hpp" +#include "orm/query/grammars/grammar.hpp" #include "orm/query/querybuilder.hpp" #include "orm/sqltransactionerror.hpp" @@ -16,8 +12,10 @@ namespace TINYORM_COMMON_NAMESPACE namespace Orm { -const char *DatabaseConnection::defaultConnectionName = const_cast("tinyorm_default"); -const char *DatabaseConnection::SAVEPOINT_NAMESPACE = const_cast("tinyorm_savepoint"); +const char * +DatabaseConnection::defaultConnectionName = const_cast("tinyorm_default"); +const char * +DatabaseConnection::SAVEPOINT_NAMESPACE = const_cast("tinyorm_savepoint"); /*! \class DatabaseConnection @@ -52,7 +50,7 @@ DatabaseConnection::DatabaseConnection( QSharedPointer DatabaseConnection::table(const QString &table, const QString &as) { - auto builder = QSharedPointer::create(*this, m_queryGrammar); + auto builder = QSharedPointer::create(*this, *m_queryGrammar); builder->from(table, as); @@ -61,7 +59,7 @@ DatabaseConnection::table(const QString &table, const QString &as) QSharedPointer DatabaseConnection::query() { - return QSharedPointer::create(*this, m_queryGrammar); + return QSharedPointer::create(*this, *m_queryGrammar); } bool DatabaseConnection::beginTransaction() @@ -404,11 +402,42 @@ QSqlQuery DatabaseConnection::getQtQuery() // TODO perf, modify bindings directly and return reference, debug impact silverqx QVector -DatabaseConnection::prepareBindings(const QVector &bindings) const +DatabaseConnection::prepareBindings(QVector bindings) const { // const auto &grammar = getQueryGrammar(); - // TODO future, here will be implemented datetime values silverqx + for (auto &binding : bindings) { + // Nothing to convert + if (!binding.isValid() || binding.isNull()) + continue; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (binding.typeId()) { +#else + switch (binding.userType()) { +#endif + /* We need to transform all instances of DateTimeInterface into the actual + date string. Each query grammar maintains its own date string format + so we'll just ask the grammar for the format to get from the date. */ + // CUR solve this datetime problem silverqx +// case QMetaType::QTime: +// case QMetaType::QDate: +// case QMetaType::QDateTime: +//// if ($value instanceof DateTimeInterface) { +//// $bindings[$key] = $value->format($grammar->getDateFormat()); +// break; + + /* Even if eg. the MySQL driver handles this internally, I will do it this way + to be consistent across all supported databases. */ + case QMetaType::Bool: + binding.convert(QMetaType::Int); + break; + + default: + break; + } + } + return bindings; } @@ -463,60 +492,9 @@ DatabaseConnection::hitTransactionalCounters(const QElapsedTimer &timer) bool DatabaseConnection::pingDatabase() { - auto qtConnection = getQtConnection(); - - const auto getMysqlHandle = [&qtConnection]() -> MYSQL * - { - if (auto driverHandle = qtConnection.driver()->handle(); - qstrcmp(driverHandle.typeName(), "MYSQL*") == 0 - ) - return *static_cast(driverHandle.data()); - - return nullptr; - }; - - const auto mysqlPing = [getMysqlHandle]() -> bool - { - auto mysqlHandle = getMysqlHandle(); - if (mysqlHandle == nullptr) - return false; - - const auto ping = mysql_ping(mysqlHandle); - const auto errNo = mysql_errno(mysqlHandle); - - /* So strange logic, because I want interpret CR_COMMANDS_OUT_OF_SYNC errno as - successful ping. */ - if (ping != 0 && errNo == CR_COMMANDS_OUT_OF_SYNC) { - // TODO log to file, how often this happen silverqx - qWarning("mysql_ping() returned : CR_COMMANDS_OUT_OF_SYNC(%ud)", errNo); - return true; - } - else if (ping == 0) - return true; - else if (ping != 0) - return false; - else { - qWarning() << "Unknown behavior during mysql_ping(), this should never happen :/"; - return false; - } - }; - - if (qtConnection.isOpen() && mysqlPing()) { - logConnected(); - return true; - } - - // The database connection was lost - logDisconnected(); - - // Database connection have to be closed manually - // isOpen() check is called in MySQL driver - qtConnection.close(); - - // Reset in transaction state and the savepoints counter - resetTransactions(); - - return false; + throw OrmRuntimeError( + QStringLiteral("The '%1' database driver doesn't support ping command.") + .arg(driverName())); } void DatabaseConnection::reconnect() const @@ -554,7 +532,8 @@ QVariant DatabaseConnection::getConfig(const QString &option) const return m_config.value(option); } -QVariant DatabaseConnection::getConfig() const +const QVariantHash & +DatabaseConnection::getConfig() const { return m_config; } @@ -659,6 +638,11 @@ DatabaseConnection &DatabaseConnection::resetStatementsCounter() return *this; } +QString DatabaseConnection::driverName() +{ + return getQtConnection().driverName(); +} + void DatabaseConnection::reconnectIfMissingConnection() const { if (!m_qtConnectionResolver) { @@ -677,19 +661,6 @@ DatabaseConnection &DatabaseConnection::resetTransactions() return *this; } -QSqlQuery DatabaseConnection::prepareQuery(const QString &queryString) -{ - // Prepare query string - auto query = getQtQuery(); - - // TODO solve setForwardOnly() in DatabaseConnection class, again this problem 🤔 silverqx -// query.setForwardOnly(m_forwardOnly); - - query.prepare(queryString); - - return query; -} - void DatabaseConnection::logDisconnected() { if (m_disconnectedLogged) @@ -715,6 +686,19 @@ void DatabaseConnection::logConnected() qInfo() << "Database connected"; } +QSqlQuery DatabaseConnection::prepareQuery(const QString &queryString) +{ + // Prepare query string + auto query = getQtQuery(); + + // TODO solve setForwardOnly() in DatabaseConnection class, again this problem 🤔 silverqx +// query.setForwardOnly(m_forwardOnly); + + query.prepare(queryString); + + return query; +} + } // namespace Orm #ifdef TINYORM_COMMON_NAMESPACE } // namespace TINYORM_COMMON_NAMESPACE diff --git a/src/orm/databasemanager.cpp b/src/orm/databasemanager.cpp index 9c87f1598..4b7568d22 100644 --- a/src/orm/databasemanager.cpp +++ b/src/orm/databasemanager.cpp @@ -223,11 +223,16 @@ QStringList DatabaseManager::supportedDrivers() const { // TODO future add method to not only supported drivers, but also check if driver is available/loadable by qsqldatabase silverqx // aaaaaaaaaaaaaachjo 🤔😁 - return {"mysql"}; + return {"mysql", "sqlite"}; // return {"mysql", "pgsql", "sqlite", "sqlsrv"}; } QStringList DatabaseManager::connectionNames() const +{ + return m_config.connections.keys(); +} + +QStringList DatabaseManager::openedConnectionNames() const { QStringList names; // TODO overflow, add check code https://stackoverflow.com/questions/22184403/how-to-cast-the-size-t-to-double-or-int-c/22184657#22184657 silverqx @@ -290,7 +295,7 @@ DatabaseConnection &DatabaseManager::resetElapsedCounter(const QString &connecti bool DatabaseManager::anyCountingElapsed() { - const auto connections = connectionNames(); + const auto connections = openedConnectionNames(); for (const auto &connectionName : connections) if (connection(connectionName).countingElapsed()) @@ -301,29 +306,48 @@ bool DatabaseManager::anyCountingElapsed() void DatabaseManager::enableAllElapsedCounters() { - const auto connections = connectionNames(); - - for (const auto &connectionName : connections) - connection(connectionName).enableElapsedCounter(); + enableElapsedCounters(openedConnectionNames()); } void DatabaseManager::disableAllElapsedCounters() { - const auto connections = connectionNames(); + disableElapsedCounters(openedConnectionNames()); +} +qint64 DatabaseManager::getAllElapsedCounters() +{ + return getElapsedCounters(openedConnectionNames()); +} + +qint64 DatabaseManager::takeAllElapsedCounters() +{ + return takeElapsedCounters(openedConnectionNames()); +} + +void DatabaseManager::resetAllElapsedCounters() +{ + resetElapsedCounters(openedConnectionNames()); +} + +void DatabaseManager::enableElapsedCounters(const QStringList &connections) +{ + for (const auto &connectionName : connections) + connection(connectionName).enableElapsedCounter(); +} + +void DatabaseManager::disableElapsedCounters(const QStringList &connections) +{ for (const auto &connectionName : connections) connection(connectionName).disableElapsedCounter(); } -qint64 DatabaseManager::getAllElapsedCounters() +qint64 DatabaseManager::getElapsedCounters(const QStringList &connections) { if (!anyCountingElapsed()) return -1; qint64 elapsed = 0; - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { const auto &connection = this->connection(connectionName); @@ -334,15 +358,13 @@ qint64 DatabaseManager::getAllElapsedCounters() return elapsed; } -qint64 DatabaseManager::takeAllElapsedCounters() +qint64 DatabaseManager::takeElapsedCounters(const QStringList &connections) { if (!anyCountingElapsed()) return -1; qint64 elapsed = 0; - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { auto &connection = this->connection(connectionName); @@ -353,10 +375,8 @@ qint64 DatabaseManager::takeAllElapsedCounters() return elapsed; } -void DatabaseManager::resetAllElapsedCounters() +void DatabaseManager::resetElapsedCounters(const QStringList &connections) { - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { auto &connection = this->connection(connectionName); @@ -397,7 +417,7 @@ DatabaseConnection &DatabaseManager::resetStatementsCounter(const QString &conne bool DatabaseManager::anyCountingStatements() { - const auto connections = connectionNames(); + const auto connections = openedConnectionNames(); for (const auto &connectionName : connections) if (connection(connectionName).countingStatements()) @@ -408,29 +428,48 @@ bool DatabaseManager::anyCountingStatements() void DatabaseManager::enableAllStatementCounters() { - const auto connections = connectionNames(); - - for (const auto &connectionName : connections) - connection(connectionName).enableStatementsCounter(); + enableStatementCounters(openedConnectionNames()); } void DatabaseManager::disableAllStatementCounters() { - const auto connections = connectionNames(); + disableStatementCounters(openedConnectionNames()); +} +StatementsCounter DatabaseManager::getAllStatementCounters() +{ + return getStatementCounters(openedConnectionNames()); +} + +StatementsCounter DatabaseManager::takeAllStatementCounters() +{ + return takeStatementCounters(openedConnectionNames()); +} + +void DatabaseManager::resetAllStatementCounters() +{ + resetStatementCounters(openedConnectionNames()); +} + +void DatabaseManager::enableStatementCounters(const QStringList &connections) +{ + for (const auto &connectionName : connections) + connection(connectionName).enableStatementsCounter(); +} + +void DatabaseManager::disableStatementCounters(const QStringList &connections) +{ for (const auto &connectionName : connections) connection(connectionName).disableStatementsCounter(); } -StatementsCounter DatabaseManager::getAllStatementCounters() +StatementsCounter DatabaseManager::getStatementCounters(const QStringList &connections) { StatementsCounter counter; if (!anyCountingStatements()) return counter; - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { const auto &connection = this->connection(connectionName); @@ -446,15 +485,13 @@ StatementsCounter DatabaseManager::getAllStatementCounters() return counter; } -StatementsCounter DatabaseManager::takeAllStatementCounters() +StatementsCounter DatabaseManager::takeStatementCounters(const QStringList &connections) { StatementsCounter counter; if (!anyCountingStatements()) return counter; - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { auto &connection = this->connection(connectionName); @@ -470,10 +507,8 @@ StatementsCounter DatabaseManager::takeAllStatementCounters() return counter; } -void DatabaseManager::resetAllStatementCounters() +void DatabaseManager::resetStatementCounters(const QStringList &connections) { - const auto connections = connectionNames(); - for (const auto &connectionName : connections) { auto &connection = this->connection(connectionName); diff --git a/src/orm/db.cpp b/src/orm/db.cpp index 80225bc08..395abf3c6 100644 --- a/src/orm/db.cpp +++ b/src/orm/db.cpp @@ -62,6 +62,11 @@ QStringList DB::connectionNames() return manager().connectionNames(); } +QStringList DB::openedConnectionNames() +{ + return manager().openedConnectionNames(); +} + const QString &DB::getDefaultConnection() { return manager().getDefaultConnection(); @@ -232,6 +237,31 @@ void DB::resetAllElapsedCounters() return manager().resetAllElapsedCounters(); } +void DB::enableElapsedCounters(const QStringList &connections) +{ + manager().enableElapsedCounters(connections); +} + +void DB::disableElapsedCounters(const QStringList &connections) +{ + manager().disableElapsedCounters(connections); +} + +qint64 DB::getElapsedCounters(const QStringList &connections) +{ + return manager().getElapsedCounters(connections); +} + +qint64 DB::takeElapsedCounters(const QStringList &connections) +{ + return manager().takeElapsedCounters(connections); +} + +void DB::resetElapsedCounters(const QStringList &connections) +{ + manager().resetElapsedCounters(connections); +} + bool DB::countingStatements(const QString &connection) { return manager().connection(connection).countingStatements(); @@ -292,6 +322,31 @@ void DB::resetAllStatementCounters() return manager().resetAllStatementCounters(); } +void DB::enableStatementCounters(const QStringList &connections) +{ + manager().enableStatementCounters(connections); +} + +void DB::disableStatementCounters(const QStringList &connections) +{ + manager().disableStatementCounters(connections); +} + +StatementsCounter DB::getStatementCounters(const QStringList &connections) +{ + return manager().getStatementCounters(connections); +} + +StatementsCounter DB::takeStatementCounters(const QStringList &connections) +{ + return manager().takeStatementCounters(connections); +} + +void DB::resetStatementCounters(const QStringList &connections) +{ + manager().resetStatementCounters(connections); +} + } // namespace Orm #ifdef TINYORM_COMMON_NAMESPACE } // namespace TINYORM_COMMON_NAMESPACE diff --git a/src/orm/mysqlconnection.cpp b/src/orm/mysqlconnection.cpp index 58e38b53b..882ad7803 100644 --- a/src/orm/mysqlconnection.cpp +++ b/src/orm/mysqlconnection.cpp @@ -1,5 +1,11 @@ #include "orm/mysqlconnection.hpp" +#include + +#include "mysql.h" + +#include "orm/query/grammars/mysqlgrammar.hpp" + #ifdef TINYORM_COMMON_NAMESPACE namespace TINYORM_COMMON_NAMESPACE { @@ -13,7 +19,85 @@ MySqlConnection::MySqlConnection( const QVariantHash &config ) : DatabaseConnection(connection, database, tablePrefix, config) -{} +{ + /* We need to initialize a query grammar that is a very important part + of the database abstraction, so we initialize it to the default value + while starting. */ + useDefaultQueryGrammar(); +} + +std::unique_ptr MySqlConnection::getDefaultQueryGrammar() const +{ + return std::make_unique(); +} + +bool MySqlConnection::isMaria() +{ + // TODO tests, now add MariaDB tests, install mariadb add connection and run all the tests against mariadb too silverqx + static const auto isMaria = std::get<1>(selectOne("select version()")).value(0) + .value().contains("MariaDB"); + + return isMaria; +} + +bool MySqlConnection::pingDatabase() +{ + auto qtConnection = getQtConnection(); + + const auto getMysqlHandle = [&qtConnection]() -> MYSQL * + { + if (auto driverHandle = qtConnection.driver()->handle(); + qstrcmp(driverHandle.typeName(), "MYSQL*") == 0 + ) + return *static_cast(driverHandle.data()); + + return nullptr; + }; + + const auto mysqlPing = [getMysqlHandle]() -> bool + { + auto mysqlHandle = getMysqlHandle(); + if (mysqlHandle == nullptr) + return false; + + const auto ping = mysql_ping(mysqlHandle); + const auto errNo = mysql_errno(mysqlHandle); + + /* So strange logic, because I want interpret CR_COMMANDS_OUT_OF_SYNC errno as + successful ping. */ + if (ping != 0 && errNo == CR_COMMANDS_OUT_OF_SYNC) { + // TODO log to file, how often this happen silverqx + qWarning("mysql_ping() returned : CR_COMMANDS_OUT_OF_SYNC(%ud)", errNo); + return true; + } + else if (ping == 0) + return true; + else if (ping != 0) + return false; + else { + qWarning() << "Unknown behavior during mysql_ping(), this should never " + "happen :/"; + return false; + } + }; + + if (qtConnection.isOpen() && mysqlPing()) { + logConnected(); + return true; + } + + // The database connection was lost + logDisconnected(); + + // Database connection have to be closed manually + // isOpen() check is called in MySQL driver + qtConnection.close(); + + // Reset in transaction state and the savepoints counter + resetTransactions(); + + return false; +} } // namespace Orm #ifdef TINYORM_COMMON_NAMESPACE diff --git a/src/orm/grammar.cpp b/src/orm/query/grammars/grammar.cpp similarity index 90% rename from src/orm/grammar.cpp rename to src/orm/query/grammars/grammar.cpp index 1b446830a..2e144ce72 100644 --- a/src/orm/grammar.cpp +++ b/src/orm/query/grammars/grammar.cpp @@ -1,4 +1,4 @@ -#include "orm/grammar.hpp" +#include "orm/query/grammars/grammar.hpp" #include @@ -9,11 +9,9 @@ namespace TINYORM_COMMON_NAMESPACE { #endif -namespace Orm +namespace Orm::Query::Grammars { -using JoinClause = Query::JoinClause; - namespace { // TODO duplicate in moviedetailservice.cpp silverqx @@ -61,16 +59,26 @@ QString Grammar::compileSelect(QueryBuilder &query) const QString Grammar::compileInsert(const QueryBuilder &query, const QVector &values) const { - return compileInsert(query, values, false); + // TODO feature, insert with empty values, this code will never be triggered, because check in the QueryBuilder::insert, even all other code works correctly and support empty values silverqx + if (values.isEmpty()) + return QStringLiteral("insert into %1 default values").arg(query.getFrom()); + + return QStringLiteral("insert into %1 (%2) values %3").arg( + query.getFrom(), + // Columns are obtained only from a first QMap + columnize(values.at(0).keys()), + joinContainer(compileInsertToVector(values), + QStringLiteral(", "))); } -QString Grammar::compileInsertOrIgnore(const QueryBuilder &query, - const QVector &values) const +QString Grammar::compileInsertOrIgnore(const QueryBuilder &, + const QVector &) const { - return compileInsert(query, values, true); + throw OrmRuntimeError("This database engine does not support inserting while " + "ignoring errors."); } -QString Grammar::compileUpdate(const QueryBuilder &query, +QString Grammar::compileUpdate(QueryBuilder &query, const QVector &values) const { const auto table = query.getFrom(); @@ -78,7 +86,7 @@ QString Grammar::compileUpdate(const QueryBuilder &query, const auto wheres = compileWheres(query); return query.getJoins().isEmpty() - ? compileUpdateWithoutJoins(table, columns, wheres) + ? compileUpdateWithoutJoins(query, table, columns, wheres) : compileUpdateWithJoins(query, table, columns, wheres); } @@ -139,12 +147,12 @@ Grammar::prepareBindingsForUpdate(const BindingsMap &bindings, return preparedBindings; } -QString Grammar::compileDelete(const QueryBuilder &query) const +QString Grammar::compileDelete(QueryBuilder &query) const { const auto table = query.getFrom(); const auto wheres = compileWheres(query); - return query.getJoins().isEmpty() ? compileDeleteWithoutJoins(table, wheres) + return query.getJoins().isEmpty() ? compileDeleteWithoutJoins(query, table, wheres) : compileDeleteWithJoins(query, table, wheres); } @@ -182,9 +190,10 @@ QVector Grammar::prepareBindingsForDelete(const BindingsMap &bindings) return preparedBindings; } -QString Grammar::compileTruncate(const QueryBuilder &query) const +std::unordered_map> +Grammar::compileTruncate(const QueryBuilder &query) const { - return QStringLiteral("truncate table %1").arg(query.getFrom()); + return {{QStringLiteral("truncate table %1").arg(query.getFrom()), {}}}; } const QString &Grammar::getDateFormat() const @@ -194,6 +203,52 @@ const QString &Grammar::getDateFormat() const return cachedFormat; } +const QVector &Grammar::getOperators() const +{ + /* I make it this way, I don't declare it as pure virtual intentionally, this give + me oportunity to instantiate the Grammar class eg. in tests. */ + static const QVector cachedOperators; + + return cachedOperators; +} + +QString Grammar::compileOrders(const QueryBuilder &query) const +{ + return QStringLiteral("order by %1").arg( + joinContainer(compileOrdersToVector(query), QStringLiteral(", "))); +} + +QString Grammar::compileLimit(const QueryBuilder &query) const +{ + return QStringLiteral("limit %1").arg(query.getLimit()); +} + +QString Grammar::compileUpdateColumns(const QVector &values) const +{ + QVector compiledAssignments; + for (const auto &assignment : values) + compiledAssignments << QStringLiteral("%1 = %2").arg( + assignment.column, + parameter(assignment.value)); + + // CUR change to QStringList::join() silverqx + return joinContainer(compiledAssignments, QStringLiteral(", ")); +} + +QString +Grammar::compileUpdateWithoutJoins(const QueryBuilder &, const QString &table, + const QString &columns, const QString &wheres) const +{ + return QStringLiteral("update %1 set %2 %3").arg(table, columns, wheres); +} + +QString +Grammar::compileDeleteWithoutJoins(const QueryBuilder &, const QString &table, + const QString &wheres) const +{ + return QStringLiteral("delete from %1 %2").arg(table, wheres); +} + Grammar::SelectComponentsVector Grammar::compileComponents(const QueryBuilder &query) const { @@ -359,6 +414,7 @@ Grammar::getWhereMethod(const WhereType whereType) const getBind(&Grammar::whereNull), getBind(&Grammar::whereNotNull), }; + static const auto size = cached.size(); // Check if whereType is in the range, just for sure 😏 @@ -422,24 +478,15 @@ QString Grammar::whereNotNull(const WhereConditionItem &where) const return QStringLiteral("%1 is not null").arg(where.column); } -QString Grammar::compileOrders(const QueryBuilder &query) const -{ - return QStringLiteral("order by %1").arg( - joinContainer(compileOrdersToVector(query), QStringLiteral(", "))); -} - QVector Grammar::compileOrdersToVector(const QueryBuilder &query) const { QVector compiledOrders; + for (const auto &order : query.getOrders()) compiledOrders << QStringLiteral("%1 %2") .arg(order.column, order.direction.toLower()); - return compiledOrders; -} -QString Grammar::compileLimit(const QueryBuilder &query) const -{ - return QStringLiteral("limit %1").arg(query.getLimit()); + return compiledOrders; } QString Grammar::compileOffset(const QueryBuilder &query) const @@ -454,12 +501,33 @@ Grammar::compileInsertToVector(const QVector &values) const to the query. Each insert should have the exact same amount of parameter bindings so we will loop through the record and parameterize them all. */ QVector compiledParameters; + for (const auto &valuesMap : values) compiledParameters << QStringLiteral("(%1)").arg(parametrize(valuesMap)); return compiledParameters; } +QString +Grammar::compileUpdateWithJoins(const QueryBuilder &query, const QString &table, + const QString &columns, const QString &wheres) const +{ + const auto joins = compileJoins(query); + + return QStringLiteral("update %1 %2 set %3 %4").arg(table, joins, columns, wheres); +} + +QString Grammar::compileDeleteWithJoins(const QueryBuilder &query, const QString &table, + const QString &wheres) const +{ + const auto alias = getAliasFromFrom(table); + + const auto joins = compileJoins(query); + + /* Alias has to be after the delete keyword and aliased table definition after the + from keyword. */ + return QStringLiteral("delete %1 from %2 %3 %4").arg(alias, table, joins, wheres); +} QString Grammar::columnize(const QStringList &columns) const { @@ -479,9 +547,7 @@ QString Grammar::columnize(const QStringList &columns, const bool isTorrentsTabl // "total_leechers, remaining, added_on, hash, status, " // "movie_detail_index, savepath" // }; - static const QString cached { - "id, name, size, progress, added_on, hash" - }; + static const QString cached {"id, name, size, progress, added_on, hash"}; return cached; } @@ -492,6 +558,7 @@ template QString Grammar::parametrize(const Container &values) const { QVector compiledParameters; + for (const auto &value : values) compiledParameters << parameter(value); @@ -515,63 +582,14 @@ QString Grammar::removeLeadingBoolean(QString statement) const ""); } -QString -Grammar::compileInsert(const QueryBuilder &query, const QVector &values, - const bool ignore) const +QString Grammar::getAliasFromFrom(const QString &from) const { - Q_ASSERT(values.size() > 0); - - return QStringLiteral("insert%1 into %2 (%3) values %4").arg( - ignore ? QStringLiteral(" ignore") : "", - query.getFrom(), - // Columns are obtained only from a first QMap - columnize(values.at(0).keys()), - joinContainer(compileInsertToVector(values), - QStringLiteral(", "))); + return from.split(QStringLiteral(" as ")).last().trimmed(); } -QString Grammar::compileUpdateColumns(const QVector &values) const +QString Grammar::unqualifyColumn(const QString &column) const { - QVector compiledAssignments; - for (const auto &assignment : values) - compiledAssignments << QStringLiteral("%1 = %2").arg( - assignment.column, - parameter(assignment.value)); - - return joinContainer(compiledAssignments, QStringLiteral(", ")); -} - -QString Grammar::compileUpdateWithoutJoins(const QString &table, const QString &columns, - const QString &wheres) const -{ - return QStringLiteral("update %1 set %2 %3").arg(table, columns, wheres); -} - -QString -Grammar::compileUpdateWithJoins(const QueryBuilder &query, const QString &table, - const QString &columns, const QString &wheres) const -{ - const auto joins = compileJoins(query); - - return QStringLiteral("update %1 %2 set %3 %4").arg(table, joins, columns, wheres); -} - -QString -Grammar::compileDeleteWithoutJoins(const QString &table, const QString &wheres) const -{ - return QStringLiteral("delete from %1 %2").arg(table, wheres); -} - -QString Grammar::compileDeleteWithJoins(const QueryBuilder &query, const QString &table, - const QString &wheres) const -{ - const auto alias = table.split(QStringLiteral(" as ")).last().trimmed(); - - const auto joins = compileJoins(query); - - /* Alias has to be after the delete keyword and aliased table definition after the - from keyword. */ - return QStringLiteral("delete %1 from %2 %3 %4").arg(alias, table, joins, wheres); + return column.split(QChar('.')).last().trimmed(); } } // namespace Orm diff --git a/src/orm/query/grammars/mysqlgrammar.cpp b/src/orm/query/grammars/mysqlgrammar.cpp new file mode 100644 index 000000000..4e23cb195 --- /dev/null +++ b/src/orm/query/grammars/mysqlgrammar.cpp @@ -0,0 +1,74 @@ +#include "orm/query/grammars/mysqlgrammar.hpp" + +#include "orm/query/querybuilder.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Query::Grammars +{ + +QString MySqlGrammar::compileInsert(const QueryBuilder &query, + const QVector &values) const +{ + // MySQL doesn't support 'default values' statement + if (values.isEmpty()) + return Grammar::compileInsert(query, {{}}); + + return Grammar::compileInsert(query, values); +} + +QString MySqlGrammar::compileInsertOrIgnore(const QueryBuilder &query, + const QVector &values) const +{ + return compileInsert(query, values).replace(0, 6, QStringLiteral("insert ignore")); +} + +const QVector &MySqlGrammar::getOperators() const +{ + static const QVector cachedOperators {"sounds like"}; + + return cachedOperators; +} + +QString +MySqlGrammar::compileUpdateWithoutJoins( + const QueryBuilder &query, const QString &table, + const QString &columns, const QString &wheres) const +{ + auto sql = Grammar::compileUpdateWithoutJoins(query, table, columns, wheres); + + /* When using MySQL, udpate statements may contain order by statements and limits + so we will compile both of those here. */ + if (!query.getOrders().isEmpty()) + sql += QChar(' ') + compileOrders(query); + + if (query.getLimit() > -1) + sql += QChar(' ') + compileLimit(query); + + return sql; +} + +QString +MySqlGrammar::compileDeleteWithoutJoins(const QueryBuilder &query, const QString &table, + const QString &wheres) const +{ + auto sql = Grammar::compileDeleteWithoutJoins(query, table, wheres); + + /* When using MySQL, delete statements may contain order by statements and limits + so we will compile both of those here. Once we have finished compiling this + we will return the completed SQL statement so it will be executed for us. */ + if (!query.getOrders().isEmpty()) + sql += QChar(' ') + compileOrders(query); + + if (query.getLimit() > -1) + sql += QChar(' ') + compileLimit(query); + + return sql; +} + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif diff --git a/src/orm/query/grammars/sqlitegrammar.cpp b/src/orm/query/grammars/sqlitegrammar.cpp new file mode 100644 index 000000000..e4f311c74 --- /dev/null +++ b/src/orm/query/grammars/sqlitegrammar.cpp @@ -0,0 +1,101 @@ +#include "orm/query/grammars/sqlitegrammar.hpp" + +#include "orm/query/querybuilder.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm::Query::Grammars +{ + +QString SQLiteGrammar::compileInsertOrIgnore(const QueryBuilder &query, + const QVector &values) const +{ + return compileInsert(query, values) + .replace(0, 6, QStringLiteral("insert or ignore")); +} + +QString SQLiteGrammar::compileUpdate(QueryBuilder &query, + const QVector &values) const +{ + if (!query.getJoins().isEmpty() || query.getLimit() > -1) + return compileUpdateWithJoinsOrLimit(query, values); + + return Grammar::compileUpdate(query, values); +} + +QString SQLiteGrammar::compileDelete(QueryBuilder &query) const +{ + if (!query.getJoins().isEmpty() || query.getLimit() > -1) + return compileDeleteWithJoinsOrLimit(query); + + return Grammar::compileDelete(query); +} + +std::unordered_map> +SQLiteGrammar::compileTruncate(const QueryBuilder &query) const +{ + const auto &table = query.getFrom(); + + return { + {"delete from sqlite_sequence where name = ?", {table}}, + {QStringLiteral("delete from %1").arg(table), {}}, + }; +} + +const QVector &SQLiteGrammar::getOperators() const +{ + static const QVector cachedOperators { + "=", "<", ">", "<=", ">=", "<>", "!=", + "like", "not like", "ilike", + "&", "|", "<<", ">>", + }; + + return cachedOperators; +} + +QString SQLiteGrammar::compileUpdateColumns(const QVector &values) const +{ + QStringList compiledAssignments; + + for (const auto &assignment : values) + compiledAssignments << QStringLiteral("%1 = %2").arg( + unqualifyColumn(assignment.column), + parameter(assignment.value)); + + return compiledAssignments.join(", "); +} + +QString +SQLiteGrammar::compileUpdateWithJoinsOrLimit(QueryBuilder &query, + const QVector &values) const +{ + const auto &table = query.getFrom(); + + const auto columns = compileUpdateColumns(values); + + const auto alias = getAliasFromFrom(table); + + const auto selectSql = compileSelect(query.select(alias + ".rowid")); + + return QStringLiteral("update %1 set %2 where %3 in (%4)") + .arg(table, columns, "rowid", selectSql); +} + +QString SQLiteGrammar::compileDeleteWithJoinsOrLimit(QueryBuilder &query) const +{ + const auto &table = query.getFrom(); + + const auto alias = getAliasFromFrom(table); + + const auto selectSql = compileSelect(query.select(alias + ".rowid")); + + return QStringLiteral("delete from %1 where %2 in (%3)") + .arg(table, "rowid", selectSql); +} + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif diff --git a/src/orm/query/querybuilder.cpp b/src/orm/query/querybuilder.cpp index 5dfacb7ad..47c107864 100644 --- a/src/orm/query/querybuilder.cpp +++ b/src/orm/query/querybuilder.cpp @@ -15,7 +15,7 @@ namespace TINYORM_COMMON_NAMESPACE namespace Orm::Query { -Builder::Builder(ConnectionInterface &connection, const Grammar &grammar) +Builder::Builder(ConnectionInterface &connection, const QueryGrammar &grammar) : m_connection(connection) , m_grammar(grammar) {} @@ -165,9 +165,10 @@ std::tuple Builder::remove(const quint64 id) return remove(); } -std::tuple Builder::truncate() +void Builder::truncate() { - return m_connection.statement(m_grammar.compileTruncate(*this)); + for (const auto &[sql, bindings] : m_grammar.compileTruncate(*this)) + m_connection.statement(sql, bindings); } Builder &Builder::select(const QStringList columns) @@ -222,6 +223,7 @@ Builder &Builder::join(const QString &table, const QString &first, case JoinType::WHERE: join->where(first, comparison, second); break; + case JoinType::ON: join->on(first, comparison, second); break; @@ -655,11 +657,10 @@ Builder &Builder::addNestedWhereQuery(const QSharedPointer &query, bool Builder::invalidOperator(const QString &comparison) const { - const auto contains = m_operators.contains(comparison); + const auto comparison_ = comparison.toLower(); - Q_ASSERT(contains); - - return contains == false; + return !m_operators.contains(comparison_) && + !m_grammar.getOperators().contains(comparison_); } Builder &Builder::addBinding(const QVariant &binding, const BindingType type) diff --git a/src/orm/sqliteconnection.cpp b/src/orm/sqliteconnection.cpp new file mode 100644 index 000000000..ca354fa64 --- /dev/null +++ b/src/orm/sqliteconnection.cpp @@ -0,0 +1,33 @@ +#include "orm/sqliteconnection.hpp" + +#include "orm/query/grammars/sqlitegrammar.hpp" + +#ifdef TINYORM_COMMON_NAMESPACE +namespace TINYORM_COMMON_NAMESPACE +{ +#endif +namespace Orm +{ + +SQLiteConnection::SQLiteConnection( + const std::function &connection, + const QString &database, const QString tablePrefix, + const QVariantHash &config +) + : DatabaseConnection(connection, database, tablePrefix, config) +{ + /* We need to initialize a query grammar that is a very important part + of the database abstraction, so we initialize it to the default value + while starting. */ + useDefaultQueryGrammar(); +} + +std::unique_ptr SQLiteConnection::getDefaultQueryGrammar() const +{ + return std::make_unique(); +} + +} // namespace Orm +#ifdef TINYORM_COMMON_NAMESPACE +} // namespace TINYORM_COMMON_NAMESPACE +#endif diff --git a/src/orm/support/configurationoptionsparser.cpp b/src/orm/support/configurationoptionsparser.cpp index ac0dd63fc..39b5f5a50 100644 --- a/src/orm/support/configurationoptionsparser.cpp +++ b/src/orm/support/configurationoptionsparser.cpp @@ -24,7 +24,8 @@ ConfigurationOptionsParser::parseConfiguration(const QVariantHash &config) const // Validate options type in the connection configuration validateConfigOptions(configOptions); - // Prepare options for prepareConfigOptions() function, convert to QVariantHash if needed + /* Prepare options for prepareConfigOptions() function, convert to QVariantHash + if needed. */ QVariantHash preparedConfigOptions = prepareConfigOptions(configOptions); // Parse config connection options, driver specific validation/modification diff --git a/src/src.pri b/src/src.pri index 8eac6d07f..ce6e3b518 100644 --- a/src/src.pri +++ b/src/src.pri @@ -6,18 +6,22 @@ SOURCES += \ $$PWD/orm/connectors/connectionfactory.cpp \ $$PWD/orm/connectors/connector.cpp \ $$PWD/orm/connectors/mysqlconnector.cpp \ + $$PWD/orm/connectors/sqliteconnector.cpp \ $$PWD/orm/databaseconnection.cpp \ $$PWD/orm/databasemanager.cpp \ $$PWD/orm/db.cpp \ - $$PWD/orm/grammar.cpp \ $$PWD/orm/logquery.cpp \ $$PWD/orm/mysqlconnection.cpp \ $$PWD/orm/ormtypes.cpp \ $$PWD/orm/query/expression.cpp \ + $$PWD/orm/query/grammars/grammar.cpp \ + $$PWD/orm/query/grammars/mysqlgrammar.cpp \ + $$PWD/orm/query/grammars/sqlitegrammar.cpp \ $$PWD/orm/query/joinclause.cpp \ $$PWD/orm/query/querybuilder.cpp \ $$PWD/orm/queryerror.cpp \ $$PWD/orm/sqlerror.cpp \ + $$PWD/orm/sqliteconnection.cpp \ $$PWD/orm/support/configurationoptionsparser.cpp \ $$PWD/orm/tiny/basemodel.cpp \ $$PWD/orm/tiny/modelnotfounderror.cpp \ diff --git a/src/src.pro b/src/src.pro index 178e7a37e..d2bc77aff 100644 --- a/src/src.pro +++ b/src/src.pro @@ -37,6 +37,11 @@ DEFINES += TINYORM_DEBUG_SQL # Build as shared library DEFINES += TINYORM_BUILDING_SHARED +# Enable code needed by tests, eg connection overriding in the BaseModel +build_tests { + DEFINES += TINYORM_TESTS_CODE +} + # Dependencies include and library paths # --- diff --git a/tests/auto/functional/orm/query/querybuilder/tst_querybuilder.cpp b/tests/auto/functional/orm/query/querybuilder/tst_querybuilder.cpp index e64a36453..bf3363036 100644 --- a/tests/auto/functional/orm/query/querybuilder/tst_querybuilder.cpp +++ b/tests/auto/functional/orm/query/querybuilder/tst_querybuilder.cpp @@ -1,53 +1,47 @@ #include +#include #include -#include "orm/grammar.hpp" +#include "orm/db.hpp" #include "orm/query/querybuilder.hpp" #include "database.hpp" -using namespace Orm; +using QueryBuilder = Orm::Query::Builder; -// TODO use QFINDTESTDATA() to load *.sql file? silverqx class tst_QueryBuilder : public QObject { Q_OBJECT -public: - tst_QueryBuilder(); - ~tst_QueryBuilder() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void find() const; void limit() const; private: - /*! Create QueryBuilder instance. */ - inline QueryBuilder createQuery() const - { return QueryBuilder(m_connection, Grammar()); } - - /*! The database connection instance. */ - ConnectionInterface &m_connection; + /*! Create QueryBuilder instance for the given connection. */ + inline QSharedPointer + createQuery(const QString &connection) const + { return DB::connection(connection).query(); } }; -tst_QueryBuilder::tst_QueryBuilder() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_QueryBuilder::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_QueryBuilder::initTestCase() -{} - -void tst_QueryBuilder::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_QueryBuilder::find() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - auto [ok, query] = builder.from("torrents").find(2); + auto builder = createQuery(connection); + + auto [ok, query] = builder->from("torrents").find(2); if (!ok) QFAIL("find() query failed."); @@ -58,10 +52,20 @@ void tst_QueryBuilder::find() const void tst_QueryBuilder::limit() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); + + if (const auto qtConnection = QSqlDatabase::database(connection); + !qtConnection.driver()->hasFeature(QSqlDriver::QuerySize) + ) + // ", " to prevent warning about variadic macro + QSKIP(QStringLiteral("'%1' driver doesn't support reporting the size " + "of a query.") + .arg(qtConnection.driverName()).toUtf8().constData(), ); + + auto builder = createQuery(connection); { - auto [ok, query] = builder.from("torrents").limit(1).get({"id"}); + auto [ok, query] = builder->from("torrents").limit(1).get({"id"}); if (!ok) QFAIL("limit(1) query failed."); @@ -70,7 +74,7 @@ void tst_QueryBuilder::limit() const } { - auto [ok, query] = builder.from("torrents").limit(3).get({"id"}); + auto [ok, query] = builder->from("torrents").limit(3).get({"id"}); if (!ok) QFAIL("limit(3) query failed."); @@ -79,7 +83,7 @@ void tst_QueryBuilder::limit() const } { - auto [ok, query] = builder.from("torrents").limit(4).get({"id"}); + auto [ok, query] = builder->from("torrents").limit(4).get({"id"}); if (!ok) QFAIL("limit(4) query failed."); diff --git a/tests/auto/functional/orm/tiny/basemodel/tst_basemodel.cpp b/tests/auto/functional/orm/tiny/basemodel/tst_basemodel.cpp index 6fa903bd8..e59f89f1c 100644 --- a/tests/auto/functional/orm/tiny/basemodel/tst_basemodel.cpp +++ b/tests/auto/functional/orm/tiny/basemodel/tst_basemodel.cpp @@ -1,26 +1,24 @@ #include +#include #include +#include "orm/db.hpp" + #include "models/setting.hpp" #include "models/torrent.hpp" #include "database.hpp" -using namespace Orm; -// TODO tests, namespace silverqx -using namespace Orm::Tiny; +using Orm::QueryError; +using Orm::Tiny::ConnectionOverride; +using Orm::Tiny::ModelNotFoundError; class tst_BaseModel : public QObject { Q_OBJECT -public: - tst_BaseModel(); - ~tst_BaseModel() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void save_Insert() const; void save_Insert_WithDefaultValues() const; @@ -77,23 +75,24 @@ private slots: void update_Failed() const; void update_SameValue() const; -private: - /*! The database connection instance. */ - ConnectionInterface &m_connection; + void truncate() const; }; -tst_BaseModel::tst_BaseModel() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_BaseModel::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_BaseModel::initTestCase() -{} - -void tst_BaseModel::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_BaseModel::save_Insert() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Torrent torrent; auto addedOn = QDateTime::fromString("2020-10-01 20:22:10", Qt::ISODate); @@ -145,6 +144,10 @@ void tst_BaseModel::save_Insert() const void tst_BaseModel::save_Insert_WithDefaultValues() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Torrent torrent; auto addedOn = QDateTime::fromString("2020-10-01 20:22:10", Qt::ISODate); @@ -193,6 +196,10 @@ void tst_BaseModel::save_Insert_WithDefaultValues() const void tst_BaseModel::save_Insert_TableWithoutAutoincrementKey() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Setting setting; setting.setAttribute("name", "setting1") @@ -231,6 +238,10 @@ void tst_BaseModel::save_Insert_TableWithoutAutoincrementKey() const void tst_BaseModel::save_Update_Success() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentFile = TorrentPreviewableFile::find(4); QVERIFY(torrentFile); QVERIFY(torrentFile->exists); @@ -253,7 +264,7 @@ void tst_BaseModel::save_Update_Success() const QCOMPARE(torrentFileFresh->getAttribute("size"), QVariant(5570)); QCOMPARE(torrentFileFresh->getAttribute("progress"), QVariant(860)); - // TODO tests, now remove + // Revert torrentFile->setAttribute("filepath", "test3_file1.mkv") .setAttribute("size", 5568) .setAttribute("progress", 870); @@ -262,9 +273,14 @@ void tst_BaseModel::save_Update_Success() const void tst_BaseModel::save_Update_WithNullValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto peer = TorrentPeer::find(4); QVERIFY(peer); QVERIFY(peer->exists); + QCOMPARE(peer->getAttribute("total_seeds"), QVariant(4)); peer->setAttribute("total_seeds", QVariant()); @@ -282,19 +298,28 @@ void tst_BaseModel::save_Update_WithNullValue() const QVERIFY(peerVerify->getAttribute("total_seeds").isValid()); QVERIFY(peerVerify->getAttribute("total_seeds").isNull()); + /* SQLite doesn't return correct underlying type in the QVariant for null values + like MySQL driver does, skip this compare for the SQLite database. */ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QCOMPARE(peerVerify->getAttribute("total_seeds"), QVariant(QMetaType(QMetaType::Int))); + if (DB::connection(connection).driverName() != "QSQLITE") + QCOMPARE(peerVerify->getAttribute("total_seeds"), + QVariant(QMetaType(QMetaType::Int))); #else - QCOMPARE(peerVerify->getAttribute("total_seeds"), QVariant(QVariant::Int)); + if (DB::connection(connection).driverName() != "QSQLITE") + QCOMPARE(peerVerify->getAttribute("total_seeds"), QVariant(QVariant::Int)); #endif - // TODO tests, remove silverqx + // Revert peer->setAttribute("total_seeds", 4); peer->save(); } void tst_BaseModel::save_Update_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto peer = TorrentPeer::find(3); QVERIFY(peer); QVERIFY(peer->exists); @@ -308,6 +333,10 @@ void tst_BaseModel::save_Update_Failed() const void tst_BaseModel::remove() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentFile = TorrentPreviewableFile::find(7); QVERIFY(torrentFile); @@ -330,6 +359,10 @@ void tst_BaseModel::remove() const void tst_BaseModel::destroy() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentFile = TorrentPreviewableFile::find(8); QVERIFY(torrentFile); @@ -358,6 +391,10 @@ void tst_BaseModel::destroy() const void tst_BaseModel::destroyWithVector() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentFiles = TorrentPreviewableFile::where({{"id", 7, "="}, {"id", 8, "=", "or"}})->get(); @@ -398,6 +435,10 @@ void tst_BaseModel::destroyWithVector() const void tst_BaseModel::all() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrents = Torrent::all(); QCOMPARE(torrents.size(), 6); @@ -409,6 +450,10 @@ void tst_BaseModel::all() const void tst_BaseModel::all_Columns() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrents = Torrent::all(); @@ -426,6 +471,10 @@ void tst_BaseModel::all_Columns() const void tst_BaseModel::latest() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrents = Torrent::latest()->get(); const auto createdAtColumn = Torrent::getCreatedAtColumn(); @@ -442,6 +491,10 @@ void tst_BaseModel::latest() const void tst_BaseModel::oldest() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrents = Torrent::oldest()->get(); const auto createdAtColumn = Torrent::getCreatedAtColumn(); @@ -458,6 +511,10 @@ void tst_BaseModel::oldest() const void tst_BaseModel::where() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::where("id", "=", 3)->first(); QVERIFY(torrent); @@ -475,6 +532,10 @@ void tst_BaseModel::where() const void tst_BaseModel::whereEq() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + // number { auto torrent = Torrent::whereEq("id", 3)->first(); @@ -489,6 +550,10 @@ void tst_BaseModel::whereEq() const } // QDateTime { + // CUR tests, sqlite datetime silverqx + if (DB::connection(connection).driverName() == "QSQLITE") + return; + auto torrent = Torrent::whereEq( "added_on", QDateTime::fromString("2020-08-01 20:11:10", Qt::ISODate)) @@ -500,6 +565,10 @@ void tst_BaseModel::whereEq() const void tst_BaseModel::where_WithVector() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::where({{"id", 3}})->first(); QVERIFY(torrent); @@ -517,6 +586,10 @@ void tst_BaseModel::where_WithVector() const void tst_BaseModel::where_WithVector_Condition() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrents = Torrent::where({{"size", 14}, {"progress", 400}})->get(); QCOMPARE(torrents.size(), 1); @@ -539,16 +612,29 @@ void tst_BaseModel::where_WithVector_Condition() const void tst_BaseModel::arrayOperator() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::where("id", "=", 2)->first(); QVERIFY(torrent); QCOMPARE((*torrent)["id"], QVariant(2)); QCOMPARE((*torrent)["name"], QVariant("test2")); + + // CUR tests, sqlite datetime silverqx + if (DB::connection(connection).driverName() == "QSQLITE") + return; + QCOMPARE((*torrent)["added_on"], QVariant(QDateTime::fromString("2020-08-02 20:11:10", Qt::ISODate))); } void tst_BaseModel::find() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(torrent); QCOMPARE(torrent->getAttribute("id"), QVariant(3)); @@ -556,6 +642,10 @@ void tst_BaseModel::find() const void tst_BaseModel::findOrNew_Found() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::findOrNew(3); @@ -576,6 +666,10 @@ void tst_BaseModel::findOrNew_Found() const void tst_BaseModel::findOrNew_NotFound() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::findOrNew(999999); @@ -596,6 +690,10 @@ void tst_BaseModel::findOrNew_NotFound() const void tst_BaseModel::findOrFail_Found() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::findOrFail(3); @@ -616,6 +714,10 @@ void tst_BaseModel::findOrFail_Found() const void tst_BaseModel::findOrFail_NotFoundFailed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + QVERIFY_EXCEPTION_THROWN(Torrent::findOrFail(999999), ModelNotFoundError); QVERIFY_EXCEPTION_THROWN(Torrent::findOrFail(999999, {"id", "name"}), @@ -624,6 +726,10 @@ void tst_BaseModel::findOrFail_NotFoundFailed() const void tst_BaseModel::firstWhere() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrentFile3 = TorrentPreviewableFile::firstWhere("id", "=", 3); @@ -642,6 +748,10 @@ void tst_BaseModel::firstWhere() const void tst_BaseModel::firstWhereEq() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentFile3 = TorrentPreviewableFile::firstWhereEq("id", 3); QVERIFY(torrentFile3->exists); @@ -651,6 +761,10 @@ void tst_BaseModel::firstWhereEq() const void tst_BaseModel::firstOrNew_Found() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::firstOrNew({{"id", 3}}); @@ -680,6 +794,10 @@ void tst_BaseModel::firstOrNew_Found() const void tst_BaseModel::firstOrNew_NotFound() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::firstOrNew({{"id", 100}}); @@ -707,6 +825,10 @@ void tst_BaseModel::firstOrNew_NotFound() const void tst_BaseModel::firstOrCreate_Found() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::firstOrCreate({{"id", 3}}); @@ -740,6 +862,10 @@ void tst_BaseModel::firstOrCreate_Found() const void tst_BaseModel::firstOrCreate_NotFound() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + const auto addedOn = QDateTime::currentDateTime(); auto torrent = Torrent::firstOrCreate( @@ -767,6 +893,10 @@ void tst_BaseModel::firstOrCreate_NotFound() const void tst_BaseModel::isCleanAndIsDirty() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(torrent->isClean()); @@ -799,6 +929,10 @@ void tst_BaseModel::isCleanAndIsDirty() const void tst_BaseModel::wasChanged() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(!torrent->wasChanged()); @@ -822,6 +956,10 @@ void tst_BaseModel::wasChanged() const void tst_BaseModel::is() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent2_1 = Torrent::find(2); auto torrent2_2 = Torrent::find(2); @@ -831,6 +969,10 @@ void tst_BaseModel::is() const void tst_BaseModel::isNot() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent2_1 = Torrent::find(2); auto torrent2_2 = Torrent::find(2); auto torrent3 = Torrent::find(3); @@ -842,12 +984,19 @@ void tst_BaseModel::isNot() const QVERIFY(torrent2_1->isNot(file4)); // Different connection name - torrent2_2->setConnection("crystal"); + torrent2_2->setConnection("dummy_connection"); + /* Disable connection override, so isNot() can pickup a connection from the model + itself and not overriden connection. */ + ConnectionOverride::connection = ""; QVERIFY(torrent2_1->isNot(torrent2_2)); } void tst_BaseModel::fresh() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + // Doesn't exist { Torrent torrent; @@ -875,6 +1024,10 @@ void tst_BaseModel::fresh() const void tst_BaseModel::refresh_OnlyAttributes() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + // Doens't exist { Torrent torrent; @@ -905,6 +1058,10 @@ void tst_BaseModel::refresh_OnlyAttributes() const void tst_BaseModel::create() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto addedOn = QDateTime::fromString("2021-02-01 20:22:10", Qt::ISODate); auto torrent = Torrent::create({ @@ -942,6 +1099,10 @@ void tst_BaseModel::create() const void tst_BaseModel::create_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto addedOn = QDateTime::fromString("2021-02-01 20:22:10", Qt::ISODate); Torrent torrent; @@ -959,6 +1120,14 @@ void tst_BaseModel::create_Failed() const void tst_BaseModel::update() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + + // CUR tests, sqlite datetime silverqx + if (DB::connection(connection).driverName() == "QSQLITE") + QSKIP("QSQLITE doesn't return QDateTime QVariant, but QString.", ); + auto timeBeforeUpdate = QDateTime::currentDateTime(); // Reset milliseconds to 0 { @@ -1005,6 +1174,10 @@ void tst_BaseModel::update() const void tst_BaseModel::update_NonExistent() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Torrent torrent; auto result = torrent.update({{"progress", 333}}); @@ -1013,6 +1186,10 @@ void tst_BaseModel::update_NonExistent() const void tst_BaseModel::update_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(torrent->exists); @@ -1025,6 +1202,10 @@ void tst_BaseModel::update_Failed() const void tst_BaseModel::update_SameValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(torrent->exists); @@ -1045,6 +1226,35 @@ void tst_BaseModel::update_SameValue() const QCOMPARE(torrentVerify->getAttribute(updatedAtColumn), updatedAt); } +void tst_BaseModel::truncate() const +{ + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + + Setting setting; + setting.setAttribute("name", "truncate"); + setting.setAttribute("value", "yes"); + + QVERIFY(!setting.exists); + const auto result = setting.save(); + QVERIFY(result); + QVERIFY(setting.exists); + + // Get the fresh record from the database + auto settingToVerify = Setting::whereEq("name", "truncate")->first(); + QVERIFY(settingToVerify); + QVERIFY(settingToVerify->exists); + + // And check attributes + QCOMPARE(settingToVerify->getAttribute("name"), QVariant("truncate")); + QCOMPARE(settingToVerify->getAttribute("value"), QVariant("yes")); + + Setting::truncate(); + + QCOMPARE(Setting::all().size(), 0); +} + QTEST_MAIN(tst_BaseModel) #include "tst_basemodel.moc" diff --git a/tests/auto/functional/orm/tiny/basemodel_relations/tst_basemodel_relations.cpp b/tests/auto/functional/orm/tiny/basemodel_relations/tst_basemodel_relations.cpp index 05df62d80..02572d2d7 100644 --- a/tests/auto/functional/orm/tiny/basemodel_relations/tst_basemodel_relations.cpp +++ b/tests/auto/functional/orm/tiny/basemodel_relations/tst_basemodel_relations.cpp @@ -10,12 +10,9 @@ #include "database.hpp" -// TODO tests, namespace silverqx -//using namespace Orm::Tiny; - -using Orm::ConnectionInterface; using Orm::One; using Orm::OrmRuntimeError; +using Orm::Tiny::ConnectionOverride; using Orm::Tiny::RelationNotFoundError; using Orm::Tiny::RelationNotLoadedError; @@ -23,13 +20,8 @@ class tst_BaseModel_Relations : public QObject { Q_OBJECT -public: - tst_BaseModel_Relations(); - ~tst_BaseModel_Relations() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void getRelation_EagerLoad_ManyAndOne() const; void getRelation_EagerLoad_BelongsTo() const; @@ -64,24 +56,23 @@ private slots: void where_WithCallback() const; void orWhere_WithCallback() const; - -private: - /*! The database connection instance. */ - ConnectionInterface &m_connection; }; -tst_BaseModel_Relations::tst_BaseModel_Relations() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_BaseModel_Relations::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_BaseModel_Relations::initTestCase() -{} - -void tst_BaseModel_Relations::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_BaseModel_Relations::getRelation_EagerLoad_ManyAndOne() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -118,6 +109,10 @@ void tst_BaseModel_Relations::getRelation_EagerLoad_ManyAndOne() const void tst_BaseModel_Relations::getRelation_EagerLoad_BelongsTo() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentPeer = TorrentPeerEager::find(2); QVERIFY(torrentPeer); QVERIFY(torrentPeer->exists); @@ -132,6 +127,10 @@ void tst_BaseModel_Relations::getRelation_EagerLoad_BelongsTo() const void tst_BaseModel_Relations::getRelation_EagerLoad_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Torrent torrent; // Many relation @@ -154,12 +153,20 @@ void tst_BaseModel_Relations::getRelation_EagerLoad_Failed() const void tst_BaseModel_Relations::EagerLoad_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + QVERIFY_EXCEPTION_THROWN(TorrentEager_Failed::find(1), RelationNotFoundError); } void tst_BaseModel_Relations::getRelationValue_LazyLoad_ManyAndOne() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -196,6 +203,10 @@ void tst_BaseModel_Relations::getRelationValue_LazyLoad_ManyAndOne() const void tst_BaseModel_Relations::getRelationValue_LazyLoad_BelongsTo() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrentPeer = TorrentPeer::find(2); QVERIFY(torrentPeer); QVERIFY(torrentPeer->exists); @@ -210,6 +221,10 @@ void tst_BaseModel_Relations::getRelationValue_LazyLoad_BelongsTo() const void tst_BaseModel_Relations::getRelationValue_LazyLoad_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + // Many relation QCOMPARE((Torrent().getRelationValue("notExists")), QVector()); @@ -223,6 +238,10 @@ void tst_BaseModel_Relations::getRelationValue_LazyLoad_Failed() const void tst_BaseModel_Relations::u_with_Empty() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + Torrent torrent; QCOMPARE(torrent.getRelations().size(), 0); @@ -230,6 +249,10 @@ void tst_BaseModel_Relations::u_with_Empty() const void tst_BaseModel_Relations::with_HasOne() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::with("torrentPeer")->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -244,6 +267,10 @@ void tst_BaseModel_Relations::with_HasOne() const void tst_BaseModel_Relations::with_HasMany() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::with("torrentFiles")->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -265,6 +292,10 @@ void tst_BaseModel_Relations::with_HasMany() const void tst_BaseModel_Relations::with_BelongsTo() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto fileProperty = TorrentPreviewableFileProperty::with("torrentFile")->find(2); QVERIFY(fileProperty); QVERIFY(fileProperty->exists); @@ -279,6 +310,10 @@ void tst_BaseModel_Relations::with_BelongsTo() const void tst_BaseModel_Relations::with_NestedRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::with("torrentFiles.fileProperty")->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -315,6 +350,10 @@ void tst_BaseModel_Relations::with_NestedRelations() const void tst_BaseModel_Relations::with_Vector_MoreRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::with({{"torrentFiles"}, {"torrentPeer"}})->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -350,12 +389,20 @@ void tst_BaseModel_Relations::with_Vector_MoreRelations() const void tst_BaseModel_Relations::with_NonExistentRelation_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + QVERIFY_EXCEPTION_THROWN(Torrent::with("torrentFiles-NON_EXISTENT")->find(1), RelationNotFoundError); } void tst_BaseModel_Relations::without() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::without("torrentPeer")->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -368,6 +415,10 @@ void tst_BaseModel_Relations::without() const void tst_BaseModel_Relations::without_NestedRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::without("torrentFiles")->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -380,6 +431,10 @@ void tst_BaseModel_Relations::without_NestedRelations() const void tst_BaseModel_Relations::without_Vector_MoreRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::without({"torrentPeer", "torrentFiles"})->find(2); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -389,6 +444,10 @@ void tst_BaseModel_Relations::without_Vector_MoreRelations() const void tst_BaseModel_Relations::load() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(2); QVERIFY(torrent->getRelations().empty()); @@ -432,6 +491,10 @@ void tst_BaseModel_Relations::load() const void tst_BaseModel_Relations::load_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(2); QVERIFY(torrent->getRelations().empty()); @@ -443,6 +506,10 @@ void tst_BaseModel_Relations::load_Failed() const void tst_BaseModel_Relations::refresh_EagerLoad_OnlyRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::find(3); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -514,6 +581,10 @@ void tst_BaseModel_Relations::refresh_EagerLoad_OnlyRelations() const void tst_BaseModel_Relations::refresh_LazyLoad_OnlyRelations() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(3); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -586,6 +657,10 @@ void tst_BaseModel_Relations::refresh_LazyLoad_OnlyRelations() const void tst_BaseModel_Relations::push_EagerLoad() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = TorrentEager::find(2); QVERIFY(torrent); @@ -658,6 +733,10 @@ void tst_BaseModel_Relations::push_EagerLoad() const void tst_BaseModel_Relations::push_LazyLoad() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(2); QVERIFY(torrent); @@ -728,6 +807,10 @@ void tst_BaseModel_Relations::push_LazyLoad() const void tst_BaseModel_Relations::where_WithCallback() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto files = Torrent::find(5)->torrentFiles() ->where([](auto &query) { @@ -749,6 +832,10 @@ void tst_BaseModel_Relations::where_WithCallback() const void tst_BaseModel_Relations::orWhere_WithCallback() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto files = Torrent::find(5)->torrentFiles() ->where("progress", ">", 990) .orWhere([](auto &query) diff --git a/tests/auto/functional/orm/tiny/models/models.pri b/tests/auto/functional/orm/tiny/models/models.pri index 11dcfb603..7637274a0 100644 --- a/tests/auto/functional/orm/tiny/models/models.pri +++ b/tests/auto/functional/orm/tiny/models/models.pri @@ -10,6 +10,8 @@ HEADERS += \ $$PWD/models/filepropertyproperty.hpp \ $$PWD/models/forwards.hpp \ $$PWD/models/forwardseager.hpp \ + $$PWD/models/role.hpp \ + $$PWD/models/roleuser.hpp \ $$PWD/models/setting.hpp \ $$PWD/models/tag.hpp \ $$PWD/models/tagged.hpp \ @@ -24,3 +26,4 @@ HEADERS += \ $$PWD/models/torrentpreviewablefileeager.hpp \ $$PWD/models/torrentpreviewablefileproperty.hpp \ $$PWD/models/torrentpreviewablefilepropertyeager.hpp \ + $$PWD/models/user.hpp \ diff --git a/tests/auto/functional/orm/tiny/models/models/filepropertyproperty.hpp b/tests/auto/functional/orm/tiny/models/models/filepropertyproperty.hpp index fa6790712..b2716f051 100644 --- a/tests/auto/functional/orm/tiny/models/models/filepropertyproperty.hpp +++ b/tests/auto/functional/orm/tiny/models/models/filepropertyproperty.hpp @@ -41,11 +41,6 @@ private: // {"fileProperty"}, }; -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif - /*! All of the relationships to be touched. */ QStringList u_touches {"fileProperty"}; }; diff --git a/tests/auto/functional/orm/tiny/models/models/setting.hpp b/tests/auto/functional/orm/tiny/models/models/setting.hpp index 199e389f4..51556b732 100644 --- a/tests/auto/functional/orm/tiny/models/models/setting.hpp +++ b/tests/auto/functional/orm/tiny/models/models/setting.hpp @@ -16,11 +16,6 @@ private: /*! Indicates if the model's ID is auto-incrementing. */ bool u_incrementing = false; - -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // SETTING_H diff --git a/tests/auto/functional/orm/tiny/models/models/tag.hpp b/tests/auto/functional/orm/tiny/models/models/tag.hpp index 5b5f3df6a..ca32224da 100644 --- a/tests/auto/functional/orm/tiny/models/models/tag.hpp +++ b/tests/auto/functional/orm/tiny/models/models/tag.hpp @@ -67,11 +67,6 @@ private: {"tagProperty"}, }; -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif - /*! All of the relationships to be touched. */ QStringList u_touches {"torrents"}; }; diff --git a/tests/auto/functional/orm/tiny/models/models/tagproperty.hpp b/tests/auto/functional/orm/tiny/models/models/tagproperty.hpp index 9929cca8e..87ee994cd 100644 --- a/tests/auto/functional/orm/tiny/models/models/tagproperty.hpp +++ b/tests/auto/functional/orm/tiny/models/models/tagproperty.hpp @@ -12,11 +12,6 @@ class TagProperty final : public BaseModel /*! The table associated with the model. */ QString u_table {"tag_properties"}; - -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TAGPROPERTY_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrent.hpp b/tests/auto/functional/orm/tiny/models/models/torrent.hpp index 77a4c8b91..3a19092ba 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrent.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrent.hpp @@ -115,13 +115,14 @@ private: // {"tags"}, }; +#ifdef PROJECT_TINYORM_PLAYGROUND /*! The connection name for the model. */ -#ifdef PROJECT_TINYORM_TEST - QString u_connection {"tinyorm_mysql_tests"}; -#else - QString u_connection {"crystal"}; + QString u_connection {"mysql_alt"}; #endif + /*! The connection name for the model. */ +// QString u_connection {"sqlite"}; + /*! Indicates if the model should be timestamped. */ // bool u_timestamps = true; /*! The storage format of the model's date columns. */ diff --git a/tests/auto/functional/orm/tiny/models/models/torrenteager.hpp b/tests/auto/functional/orm/tiny/models/models/torrenteager.hpp index 9bf76bab3..ff3704fcf 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrenteager.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrenteager.hpp @@ -58,11 +58,6 @@ private: {"torrentFiles.fileProperty"}, {"torrentPeer"}, }; - - /*! The connection name for the model. */ -#ifdef PROJECT_TINYORM_TEST - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TORRENTEAGER_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrenteager_failed.hpp b/tests/auto/functional/orm/tiny/models/models/torrenteager_failed.hpp index 08acfc511..e2f33f977 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrenteager_failed.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrenteager_failed.hpp @@ -45,11 +45,6 @@ private: QVector u_with { {"torrentFiles-NON_EXISTENT"}, }; - - /*! The connection name for the model. */ -#ifdef PROJECT_TINYORM_TEST - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TORRENTEAGER_FAILED_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrentpeer.hpp b/tests/auto/functional/orm/tiny/models/models/torrentpeer.hpp index b3ecc0c6b..09ce3710b 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrentpeer.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrentpeer.hpp @@ -38,11 +38,6 @@ private: QVector u_with { // {"torrent"}, }; - -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TORRENTPEER_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrentpeereager.hpp b/tests/auto/functional/orm/tiny/models/models/torrentpeereager.hpp index a3910b319..8ca38272f 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrentpeereager.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrentpeereager.hpp @@ -38,11 +38,6 @@ private: QVector u_with { {"torrent"}, }; - -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TORRENTPEEREAGER_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrentpeereager_norelations.hpp b/tests/auto/functional/orm/tiny/models/models/torrentpeereager_norelations.hpp index 9907d7b65..b09fdea51 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrentpeereager_norelations.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrentpeereager_norelations.hpp @@ -12,11 +12,6 @@ class TorrentPeerEager_NoRelations final : public BaseModel u_with { {"fileProperty"}, }; - -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif }; #endif // TORRENTPREVIEWABLEFILEEAGER_H diff --git a/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefileproperty.hpp b/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefileproperty.hpp index fdd3af349..3d0684d25 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefileproperty.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefileproperty.hpp @@ -40,11 +40,6 @@ private: // {"torrentFile"}, }; -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif - /*! Indicates if the model should be timestamped. */ bool u_timestamps = false; diff --git a/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefilepropertyeager.hpp b/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefilepropertyeager.hpp index a7c94539c..4fc2dbd03 100644 --- a/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefilepropertyeager.hpp +++ b/tests/auto/functional/orm/tiny/models/models/torrentpreviewablefilepropertyeager.hpp @@ -14,11 +14,6 @@ class TorrentPreviewableFilePropertyEager final : /*! The table associated with the model. */ QString u_table {"torrent_previewable_file_properties"}; -#ifdef PROJECT_TINYORM_TEST - /*! The connection name for the model. */ - QString u_connection {"tinyorm_mysql_tests"}; -#endif - /*! Indicates if the model should be timestamped. */ bool u_timestamps = false; }; diff --git a/tests/auto/functional/orm/tiny/relations_inserting_updating/tst_relations_inserting_updating.cpp b/tests/auto/functional/orm/tiny/relations_inserting_updating/tst_relations_inserting_updating.cpp index 52557cbea..09dc5283d 100644 --- a/tests/auto/functional/orm/tiny/relations_inserting_updating/tst_relations_inserting_updating.cpp +++ b/tests/auto/functional/orm/tiny/relations_inserting_updating/tst_relations_inserting_updating.cpp @@ -7,20 +7,15 @@ #include "database.hpp" using Orm::AttributeItem; -using Orm::ConnectionInterface; using Orm::QueryError; +using Orm::Tiny::ConnectionOverride; class tst_Relations_Inserting_Updating : public QObject { Q_OBJECT -public: - tst_Relations_Inserting_Updating(); - ~tst_Relations_Inserting_Updating() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void save_OnHasOneOrMany() const; void save_OnHasOneOrMany_WithRValue() const; @@ -39,24 +34,23 @@ private slots: void createMany_OnHasOneOrMany_WithRValue() const; void createMany_OnHasOneOrMany_Failed() const; void createMany_OnHasOneOrMany_WithRValue_Failed() const; - -private: - /*! The database connection instance. */ - ConnectionInterface &m_connection; }; -tst_Relations_Inserting_Updating::tst_Relations_Inserting_Updating() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_Relations_Inserting_Updating::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_Relations_Inserting_Updating::initTestCase() -{} - -void tst_Relations_Inserting_Updating::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_Relations_Inserting_Updating::save_OnHasOneOrMany() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -97,6 +91,10 @@ void tst_Relations_Inserting_Updating::save_OnHasOneOrMany() const void tst_Relations_Inserting_Updating::save_OnHasOneOrMany_WithRValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -130,6 +128,10 @@ void tst_Relations_Inserting_Updating::save_OnHasOneOrMany_WithRValue() const void tst_Relations_Inserting_Updating::save_OnHasOneOrMany_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -152,6 +154,10 @@ void tst_Relations_Inserting_Updating::save_OnHasOneOrMany_Failed() const void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -220,6 +226,10 @@ void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany() const void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany_WithRValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -275,6 +285,10 @@ void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany_WithRValue() cons void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -300,6 +314,10 @@ void tst_Relations_Inserting_Updating::saveMany_OnHasOneOrMany_Failed() const void tst_Relations_Inserting_Updating::create_OnHasOneOrMany() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -334,6 +352,10 @@ void tst_Relations_Inserting_Updating::create_OnHasOneOrMany() const void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_WithRValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -366,6 +388,10 @@ void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_WithRValue() const void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -387,6 +413,10 @@ void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_Failed() const void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_WithRValue_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -407,6 +437,10 @@ void tst_Relations_Inserting_Updating::create_OnHasOneOrMany_WithRValue_Failed() void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -470,6 +504,10 @@ void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany() const void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany_WithRValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(5); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -525,6 +563,10 @@ void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany_WithRValue() co void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); @@ -554,6 +596,10 @@ void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany_Failed() const void tst_Relations_Inserting_Updating::createMany_OnHasOneOrMany_WithRValue_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrent = Torrent::find(1); QVERIFY(torrent); QVERIFY(torrent->exists); diff --git a/tests/auto/functional/orm/tiny/tinybuilder/tst_tinybuilder.cpp b/tests/auto/functional/orm/tiny/tinybuilder/tst_tinybuilder.cpp index 22a3e0a11..f5f183e96 100644 --- a/tests/auto/functional/orm/tiny/tinybuilder/tst_tinybuilder.cpp +++ b/tests/auto/functional/orm/tiny/tinybuilder/tst_tinybuilder.cpp @@ -1,25 +1,25 @@ #include #include +#include "orm/db.hpp" + #include "models/torrent.hpp" #include "database.hpp" -using namespace Orm; -// TODO tests, namespace silverqx -using namespace Orm::Tiny; +using Orm::QueryError; +using Orm::Tiny::ConnectionOverride; +using Orm::Tiny::ModelNotFoundError; + +template +using TinyBuilder = Orm::Tiny::Builder; class tst_TinyBuilder : public QObject { Q_OBJECT -public: - tst_TinyBuilder(); - ~tst_TinyBuilder() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void get() const; void get_Columns() const; @@ -42,23 +42,23 @@ private: inline static std::unique_ptr> createQuery() { return Model().query(); } - - /*! The database connection instance. */ - ConnectionInterface &m_connection; }; -tst_TinyBuilder::tst_TinyBuilder() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_TinyBuilder::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_TinyBuilder::initTestCase() -{} - -void tst_TinyBuilder::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_TinyBuilder::get() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrents = createQuery()->get(); QCOMPARE(torrents.size(), 6); @@ -72,17 +72,25 @@ void tst_TinyBuilder::get() const void tst_TinyBuilder::get_Columns() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto torrents = createQuery()->get({"id", "name", "size"}); - const auto &torrent3 = torrents.at(1); - QCOMPARE(torrent3.getAttributes().size(), 3); - QCOMPARE(torrent3.getAttributes().at(0).key, QString("id")); - QCOMPARE(torrent3.getAttributes().at(1).key, QString("name")); - QCOMPARE(torrent3.getAttributes().at(2).key, QString("size")); + const auto &torrent = torrents.at(1); + QCOMPARE(torrent.getAttributes().size(), 3); + QCOMPARE(torrent.getAttributes().at(0).key, QString("id")); + QCOMPARE(torrent.getAttributes().at(1).key, QString("name")); + QCOMPARE(torrent.getAttributes().at(2).key, QString("size")); } void tst_TinyBuilder::value() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto value = Torrent::whereEq("id", 2)->value("name"); QCOMPARE(value, QVariant("test2")); @@ -90,6 +98,10 @@ void tst_TinyBuilder::value() const void tst_TinyBuilder::value_ModelNotFound() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto value = Torrent::whereEq("id", 999999)->value("name"); QVERIFY(!value.isValid()); @@ -98,6 +110,10 @@ void tst_TinyBuilder::value_ModelNotFound() const void tst_TinyBuilder::firstOrFail_Found() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + { auto torrent = Torrent::whereEq("id", 3)->firstOrFail(); @@ -118,6 +134,10 @@ void tst_TinyBuilder::firstOrFail_Found() const void tst_TinyBuilder::firstOrFail_NotFoundFailed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + QVERIFY_EXCEPTION_THROWN(Torrent::whereEq("id", 999999)->firstOrFail(), ModelNotFoundError); QVERIFY_EXCEPTION_THROWN(Torrent::whereEq("id", 999999)->firstOrFail({"id", "name"}), @@ -126,6 +146,14 @@ void tst_TinyBuilder::firstOrFail_NotFoundFailed() const void tst_TinyBuilder::incrementAndDecrement() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + + // CUR tests, sqlite datetime silverqx + if (DB::connection(connection).driverName() == "QSQLITE") + QSKIP("QSQLITE doesn't return QDateTime QVariant, but QString.", ); + auto timeBeforeIncrement = QDateTime::currentDateTime(); // Reset milliseconds to 0 { @@ -173,6 +201,14 @@ void tst_TinyBuilder::incrementAndDecrement() const void tst_TinyBuilder::update() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + + // CUR tests, sqlite datetime silverqx + if (DB::connection(connection).driverName() == "QSQLITE") + QSKIP("QSQLITE doesn't return QDateTime QVariant, but QString.", ); + auto timeBeforeUpdate = QDateTime::currentDateTime(); // Reset milliseconds to 0 { @@ -220,6 +256,10 @@ void tst_TinyBuilder::update() const void tst_TinyBuilder::update_Failed() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + QVERIFY_EXCEPTION_THROWN( Torrent::whereEq("id", 3)->update({{"progress-NON_EXISTENT", 333}}), QueryError); @@ -227,6 +267,10 @@ void tst_TinyBuilder::update_Failed() const void tst_TinyBuilder::update_SameValue() const { + QFETCH_GLOBAL(QString, connection); + + ConnectionOverride::connection = connection; + auto timeBeforeUpdate = QDateTime::currentDateTime(); // Reset milliseconds to 0 { diff --git a/tests/auto/unit/orm/orm/databaseconnection/tst_databaseconnection.cpp b/tests/auto/unit/orm/orm/databaseconnection/tst_databaseconnection.cpp index 3f0f1a2e7..62e59bfb9 100644 --- a/tests/auto/unit/orm/orm/databaseconnection/tst_databaseconnection.cpp +++ b/tests/auto/unit/orm/orm/databaseconnection/tst_databaseconnection.cpp @@ -1,45 +1,71 @@ #include #include +#include "orm/db.hpp" +#include "orm/mysqlconnection.hpp" + #include "database.hpp" -using namespace Orm; +using Orm::MySqlConnection; class tst_DatabaseConnection : public QObject { Q_OBJECT -public: - tst_DatabaseConnection(); - ~tst_DatabaseConnection() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; - void pingDatabase(); + void pingDatabase() const; -private: - ConnectionInterface &m_connection; + void isNotMaria_OnMySqlConnection() const; }; -tst_DatabaseConnection::tst_DatabaseConnection() - : m_connection(TestUtils::Database::createConnection()) -{} - -void tst_DatabaseConnection::initTestCase() -{} - -void tst_DatabaseConnection::cleanupTestCase() -{} - -void tst_DatabaseConnection::pingDatabase() +void tst_DatabaseConnection::initTestCase_data() const { - const auto result = m_connection.pingDatabase(); + QTest::addColumn("connection"); + + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} + +void tst_DatabaseConnection::pingDatabase() const +{ + QFETCH_GLOBAL(QString, connection); + + auto &connection_ = DB::connection(connection); + + if (const auto driverName = connection_.driverName(); + driverName == "QSQLITE" + ) + QSKIP(QStringLiteral("The '%1' database driver doesn't support ping command.") + .arg(driverName).toUtf8().constData(), ); + + const auto result = connection_.pingDatabase(); QVERIFY2(result, "Ping database failed."); } +void tst_DatabaseConnection::isNotMaria_OnMySqlConnection() const +{ + QFETCH_GLOBAL(QString, connection); + + auto &connection_ = DB::connection(connection); + + if (const auto driverName = connection_.driverName(); + driverName != "QMYSQL" + ) + QSKIP(QStringLiteral("The '%1' database driver doesn't implement isMaria() " + "method.") + .arg(driverName).toUtf8().constData(), ); + + const auto expected = !(connection == "tinyorm_mysql_tests"); + + const auto isMaria = dynamic_cast(connection_).isMaria(); + + QCOMPARE(isMaria, expected); +} + QTEST_MAIN(tst_DatabaseConnection) #include "tst_databaseconnection.moc" diff --git a/tests/auto/unit/orm/orm/grammar/tst_grammar.cpp b/tests/auto/unit/orm/orm/grammar/tst_grammar.cpp index 53e7ca4ea..8c4723f01 100644 --- a/tests/auto/unit/orm/orm/grammar/tst_grammar.cpp +++ b/tests/auto/unit/orm/orm/grammar/tst_grammar.cpp @@ -1,34 +1,15 @@ #include #include -#include "orm/grammar.hpp" - class tst_Grammar : public QObject { Q_OBJECT -public: - tst_Grammar(); - ~tst_Grammar() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); - - void test_case1(); - + void test_case1() const; }; -tst_Grammar::tst_Grammar() -{} - -void tst_Grammar::initTestCase() -{} - -void tst_Grammar::cleanupTestCase() -{} - -void tst_Grammar::test_case1() +void tst_Grammar::test_case1() const {} QTEST_MAIN(tst_Grammar) diff --git a/tests/auto/unit/orm/query/querybuilder/tst_querybuilder.cpp b/tests/auto/unit/orm/query/querybuilder/tst_querybuilder.cpp index 7c6f12d30..46de13651 100644 --- a/tests/auto/unit/orm/query/querybuilder/tst_querybuilder.cpp +++ b/tests/auto/unit/orm/query/querybuilder/tst_querybuilder.cpp @@ -1,28 +1,21 @@ #include #include -#include "orm/grammar.hpp" -#include "orm/query/expression.hpp" +#include "orm/db.hpp" #include "orm/query/querybuilder.hpp" #include "database.hpp" -using namespace Orm; +using QueryBuilder = Orm::Query::Builder; +using Raw = Orm::Query::Expression; -using Raw = Query::Expression; - -// TODO test exceptions in tests, I can not find exception message during debug and the same during normal test execution, in console is only "Caught unhandled exception" and that is all 😕 silverqx +// TODO test exceptions in tests, qt doesn't care about exceptions, totally ignore it, so when the exception is thrown, I didn't get any exception message or something similar, nothing 👿, try to solve it somehow 🤔 silverqx class tst_QueryBuilder : public QObject { Q_OBJECT -public: - tst_QueryBuilder(); - ~tst_QueryBuilder() = default; - private slots: - void initTestCase(); - void cleanupTestCase(); + void initTestCase_data() const; void from() const; @@ -61,656 +54,760 @@ private slots: void whereNotNullWithVectorValue() const; private: - inline QueryBuilder createQuery() const - { return QueryBuilder(m_connection, Grammar()); } - - ConnectionInterface &m_connection; + /*! Create QueryBuilder instance for the given connection. */ + inline QSharedPointer + createQuery(const QString &connection) const + { return DB::connection(connection).query(); } }; -tst_QueryBuilder::tst_QueryBuilder() - : m_connection(TestUtils::Database::createConnection()) -{} +void tst_QueryBuilder::initTestCase_data() const +{ + QTest::addColumn("connection"); -void tst_QueryBuilder::initTestCase() -{} - -void tst_QueryBuilder::cleanupTestCase() -{} + // Run all tests for all supported database connections + for (const auto &connection : TestUtils::Database::createConnections()) + QTest::newRow(connection.toUtf8().constData()) << connection; +} void tst_QueryBuilder::from() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); + + auto builder = createQuery(connection); const auto tableEmpty = QString(""); - QCOMPARE(builder.getFrom(), tableEmpty); + QCOMPARE(builder->getFrom(), tableEmpty); const auto tableTorrents = QStringLiteral("torrents"); - builder.from(tableTorrents); + builder->from(tableTorrents); - QCOMPARE(builder.getFrom(), tableTorrents); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->getFrom(), tableTorrents); + QCOMPARE(builder->toSql(), "select * from torrents"); const auto tableTorrentPeers = QStringLiteral("torrent_peers"); - builder.from(tableTorrentPeers); + builder->from(tableTorrentPeers); - QCOMPARE(builder.getFrom(), tableTorrentPeers); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->getFrom(), tableTorrentPeers); + QCOMPARE(builder->toSql(), "select * from torrent_peers"); } void tst_QueryBuilder::select() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.select({"id", "name"}); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->select({"id", "name"}); + QCOMPARE(builder->toSql(), "select id, name from torrents"); - builder.select(); - QCOMPARE(builder.toSql(), + builder->select(); + QCOMPARE(builder->toSql(), "select * from torrents"); - builder.select("id"); - QCOMPARE(builder.toSql(), + builder->select("id"); + QCOMPARE(builder->toSql(), "select id from torrents"); } void tst_QueryBuilder::addSelect() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.addSelect({"id", "name"}); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->addSelect({"id", "name"}); + QCOMPARE(builder->toSql(), "select id, name from torrents"); - builder.addSelect("size"); - QCOMPARE(builder.toSql(), + builder->addSelect("size"); + QCOMPARE(builder->toSql(), "select id, name, size from torrents"); - builder.addSelect("*"); - QCOMPARE(builder.toSql(), + builder->addSelect("*"); + QCOMPARE(builder->toSql(), "select id, name, size, * from torrents"); } void tst_QueryBuilder::distinct() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - auto distinct = builder.getDistinct(); + builder->from("torrents"); + + auto distinct = builder->getDistinct(); QCOMPARE(distinct, false); - builder.distinct(); - distinct = builder.getDistinct(); + builder->distinct(); + distinct = builder->getDistinct(); QCOMPARE(distinct, true); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select distinct * from torrents"); - builder.select({"name", "size"}); - QCOMPARE(builder.toSql(), + builder->select({"name", "size"}); + QCOMPARE(builder->toSql(), "select distinct name, size from torrents"); } void tst_QueryBuilder::orderBy() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.orderBy("name", "asc"); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->orderBy("name", "asc"); + QCOMPARE(builder->toSql(), "select * from torrents order by name asc"); - builder.orderBy("id", "desc"); - QCOMPARE(builder.toSql(), + builder->orderBy("id", "desc"); + QCOMPARE(builder->toSql(), "select * from torrents order by name asc, id desc"); - builder.reorder() + builder->reorder() .orderByDesc("name"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents order by name desc"); - builder.reorder("id", "asc"); - QCOMPARE(builder.toSql(), + builder->reorder("id", "asc"); + QCOMPARE(builder->toSql(), "select * from torrents order by id asc"); } void tst_QueryBuilder::latestOldest() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.latest("name"); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->latest("name"); + QCOMPARE(builder->toSql(), "select * from torrents order by name desc"); - builder.reorder().oldest("name"); - QCOMPARE(builder.toSql(), + builder->reorder().oldest("name"); + QCOMPARE(builder->toSql(), "select * from torrents order by name asc"); } void tst_QueryBuilder::limitOffset() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.limit(10); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->limit(10); + QCOMPARE(builder->toSql(), "select * from torrents limit 10"); - builder.offset(5); - QCOMPARE(builder.toSql(), + builder->offset(5); + QCOMPARE(builder->toSql(), "select * from torrents limit 10 offset 5"); } void tst_QueryBuilder::takeSkip() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.take(15); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->take(15); + QCOMPARE(builder->toSql(), "select * from torrents limit 15"); - builder.skip(5); - QCOMPARE(builder.toSql(), + builder->skip(5); + QCOMPARE(builder->toSql(), "select * from torrents limit 15 offset 5"); } void tst_QueryBuilder::forPage() const { - auto builder = createQuery(); + QFETCH_GLOBAL(QString, connection); - builder.from("torrents"); + auto builder = createQuery(connection); - builder.forPage(2, 10); - QCOMPARE(builder.toSql(), + builder->from("torrents"); + + builder->forPage(2, 10); + QCOMPARE(builder->toSql(), "select * from torrents limit 10 offset 10"); - builder.forPage(5); - QCOMPARE(builder.toSql(), + builder->forPage(5); + QCOMPARE(builder->toSql(), "select * from torrents limit 30 offset 120"); } void tst_QueryBuilder::basicWhere() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "=", 3); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "=", 3); + QCOMPARE(builder->toSql(), "select * from torrents where id = ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").whereEq("id", 3); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereEq("id", 3); + QCOMPARE(builder->toSql(), "select * from torrents where id = ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").whereEq("id", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereEq("id", 3) .whereEq("name", "test3"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? and name = ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3), QVariant("test3")})); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "!=", 3); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "!=", 3); + QCOMPARE(builder->toSql(), "select * from torrents where id != ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "<>", 3); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "<>", 3); + QCOMPARE(builder->toSql(), "select * from torrents where id <> ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", ">", 3); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", ">", 3); + QCOMPARE(builder->toSql(), "select * from torrents where id > ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", ">", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", ">", 3) .where("name", "like", "test%"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id > ? and name like ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3), QVariant("test%")})); } } void tst_QueryBuilder::whereWithVectorValue() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").where({{"id", 3}}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where({{"id", 3}}); + QCOMPARE(builder->toSql(), "select * from torrents where (id = ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector {QVariant(3)}); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}); + QCOMPARE(builder->toSql(), "select * from torrents where (id = ? and size > ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3), QVariant(10)})); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}) .where({{"progress", 100, ">="}}); - QCOMPARE(builder.toSql(), - "select * from torrents where (id = ? and size > ?) and (progress >= ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrents where (id = ? and size > ?) " + "and (progress >= ?)"); + QCOMPARE(builder->getBindings(), QVector({QVariant(3), QVariant(10), QVariant(100)})); } } void tst_QueryBuilder::basicOrWhere() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", ">", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", ">", 4) .orWhere("progress", ">=", 300); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id > ? or progress >= ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(4), QVariant(300)})); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", ">", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", ">", 4) .orWhereEq("name", "test3"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id > ? or name = ?"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(4), QVariant("test3")})); } } void tst_QueryBuilder::orWhereWithVectorValue() const { - auto builder = createQuery(); - builder.select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}) + QFETCH_GLOBAL(QString, connection); + + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where({{"id", 3}, {"size", 10, ">"}}) .orWhere({{"progress", 100, ">="}}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where (id = ? and size > ?) or (progress >= ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3), QVariant(10), QVariant(100)})); } void tst_QueryBuilder::whereColumn() const { - { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files") - .whereColumn("filepath", "=", "note") - .whereColumn("size", ">=", "progress"); - QCOMPARE(builder.toSql(), - "select * from torrent_previewable_files where filepath = note and size >= progress"); - QCOMPARE(builder.getBindings(), - QVector()); - } + QFETCH_GLOBAL(QString, connection); + + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files") + .whereColumn("filepath", "=", "note") + .whereColumn("size", ">=", "progress"); + QCOMPARE(builder->toSql(), + "select * from torrent_previewable_files where filepath = note " + "and size >= progress"); + QCOMPARE(builder->getBindings(), + QVector()); } void tst_QueryBuilder::orWhereColumn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files") + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files") .whereColumnEq("filepath", "note") .orWhereColumnEq("size", "progress"); - QCOMPARE(builder.toSql(), - "select * from torrent_previewable_files where filepath = note or size = progress"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_previewable_files where filepath = note " + "or size = progress"); + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files") + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files") .whereColumnEq("filepath", "note") .orWhereColumn("size", ">", "progress"); - QCOMPARE(builder.toSql(), - "select * from torrent_previewable_files where filepath = note or size > progress"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_previewable_files where filepath = note " + "or size > progress"); + QCOMPARE(builder->getBindings(), QVector()); } } void tst_QueryBuilder::whereColumnWithVectorValue() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files") + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files") .whereColumn({{"filepath", "note"}, {"size", "progress", ">"}}); - QCOMPARE(builder.toSql(), - "select * from torrent_previewable_files where (filepath = note and size > progress)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_previewable_files where (filepath = note " + "and size > progress)"); + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files") + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files") .whereColumn({{"filepath", "note"}, {"size", "progress", ">", "or"}}); - QCOMPARE(builder.toSql(), - "select * from torrent_previewable_files where (filepath = note or size > progress)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_previewable_files where (filepath = note " + "or size > progress)"); + QCOMPARE(builder->getBindings(), QVector()); } } void tst_QueryBuilder::orWhereColumnWithVectorValue() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files").whereEq("id", 2) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files").whereEq("id", 2) .orWhereColumn({{"filepath", "note"}, {"size", "progress", ">"}}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_previewable_files " "where id = ? or (filepath = note or size > progress)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files").whereEq("id", 2) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files").whereEq("id", 2) .orWhereColumn({{"filepath", "note"}, {"size", "progress", ">", "and"}}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_previewable_files " "where id = ? or (filepath = note and size > progress)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_previewable_files").whereEq("id", 2) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_previewable_files").whereEq("id", 2) .orWhereColumn({{"filepath", "note"}, {"size", "progress", ">", "or"}}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_previewable_files " "where id = ? or (filepath = note or size > progress)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2)})); } } void tst_QueryBuilder::basicWhereIn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").whereIn("id", {2, 3, 4}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereIn("id", {2, 3, 4}); + QCOMPARE(builder->toSql(), "select * from torrents where id in (?, ?, ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2), QVariant(3), QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "=", 1) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "=", 1) .orWhereIn("id", {2, 3, 4}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? or id in (?, ?, ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(1), QVariant(2), QVariant(3), QVariant(4)})); } } void tst_QueryBuilder::basicWhereNotIn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").whereNotIn("id", {2, 3, 4}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereNotIn("id", {2, 3, 4}); + QCOMPARE(builder->toSql(), "select * from torrents where id not in (?, ?, ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2), QVariant(3), QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "=", 1) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "=", 1) .orWhereNotIn("id", {2, 3, 4}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? or id not in (?, ?, ?)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(1), QVariant(2), QVariant(3), QVariant(4)})); } } void tst_QueryBuilder::emptyWhereIn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").whereIn("id", {}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereIn("id", {}); + QCOMPARE(builder->toSql(), "select * from torrents where 0 = 1"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "=", 1) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "=", 1) .orWhereIn("id", {}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? or 0 = 1"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(1)})); } } void tst_QueryBuilder::emptyNotWhereIn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").whereNotIn("id", {}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereNotIn("id", {}); + QCOMPARE(builder->toSql(), "select * from torrents where 1 = 1"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrents").where("id", "=", 1) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").where("id", "=", 1) .orWhereNotIn("id", {}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? or 1 = 1"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(1)})); } } void tst_QueryBuilder::rawWhereIn() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrents").whereIn("id", {Raw(3)}); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereIn("id", {Raw(3)}); + QCOMPARE(builder->toSql(), "select * from torrents where id in (3)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrents").whereEq("id", {2}) + auto builder = createQuery(connection); + + builder->select("*").from("torrents").whereEq("id", {2}) .orWhereIn("id", {Raw(3)}); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrents where id = ? or id in (3)"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(2)})); } } void tst_QueryBuilder::basicWhereNull() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereNull("seeds"); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereNull("seeds"); + QCOMPARE(builder->toSql(), "select * from torrent_peers where seeds is null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 4) .whereNull("seeds"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_peers where id = ? and seeds is null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 3) .orWhereNull("seeds"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_peers where id = ? or seeds is null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3)})); } } void tst_QueryBuilder::basicWhereNotNull() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereNotNull("seeds"); - QCOMPARE(builder.toSql(), + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereNotNull("seeds"); + QCOMPARE(builder->toSql(), "select * from torrent_peers where seeds is not null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 4) .whereNotNull("seeds"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_peers where id = ? and seeds is not null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 3) .orWhereNotNull("seeds"); - QCOMPARE(builder.toSql(), + QCOMPARE(builder->toSql(), "select * from torrent_peers where id = ? or seeds is not null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->getBindings(), QVector({QVariant(3)})); } } void tst_QueryBuilder::whereNullWithVectorValue() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where seeds is null and total_seeds is null"); - QCOMPARE(builder.getBindings(), + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereNull({"seeds", "total_seeds"}); + QCOMPARE(builder->toSql(), + "select * from torrent_peers where seeds is null " + "and total_seeds is null"); + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 4) .whereNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where id = ? and seeds is null and total_seeds is null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_peers where id = ? and seeds is null " + "and total_seeds is null"); + QCOMPARE(builder->getBindings(), QVector({QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 3) .orWhereNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where id = ? or seeds is null or total_seeds is null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_peers where id = ? or seeds is null " + "or total_seeds is null"); + QCOMPARE(builder->getBindings(), QVector({QVariant(3)})); } } void tst_QueryBuilder::whereNotNullWithVectorValue() const { + QFETCH_GLOBAL(QString, connection); + { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereNotNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where seeds is not null and total_seeds is not null"); - QCOMPARE(builder.getBindings(), + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereNotNull({"seeds", "total_seeds"}); + QCOMPARE(builder->toSql(), + "select * from torrent_peers where seeds is not null " + "and total_seeds is not null"); + QCOMPARE(builder->getBindings(), QVector()); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 4) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 4) .whereNotNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where id = ? and seeds is not null and total_seeds is not null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_peers where id = ? and seeds is not null " + "and total_seeds is not null"); + QCOMPARE(builder->getBindings(), QVector({QVariant(4)})); } { - auto builder = createQuery(); - builder.select("*").from("torrent_peers").whereEq("id", 3) + auto builder = createQuery(connection); + + builder->select("*").from("torrent_peers").whereEq("id", 3) .orWhereNotNull({"seeds", "total_seeds"}); - QCOMPARE(builder.toSql(), - "select * from torrent_peers where id = ? or seeds is not null or total_seeds is not null"); - QCOMPARE(builder.getBindings(), + QCOMPARE(builder->toSql(), + "select * from torrent_peers where id = ? or seeds is not null " + "or total_seeds is not null"); + QCOMPARE(builder->getBindings(), QVector({QVariant(3)})); } } diff --git a/tests/auto/utils/src/database.cpp b/tests/auto/utils/src/database.cpp index bf3aa7407..39866ff7b 100644 --- a/tests/auto/utils/src/database.cpp +++ b/tests/auto/utils/src/database.cpp @@ -5,35 +5,49 @@ namespace TestUtils { -Database::Database() -{} - -Orm::ConnectionInterface &Database::createConnection() +const QStringList &Database::createConnections() { - static const auto connectionName = QStringLiteral("tinyorm_mysql_tests"); + // Ownership of a unique_ptr() + static const auto manager = DB::create({ + {"tinyorm_mysql_tests", { + {"driver", "QMYSQL"}, + {"host", qEnvironmentVariable("DB_MYSQL_HOST", "127.0.0.1")}, + {"port", qEnvironmentVariable("DB_MYSQL_PORT", "3306")}, + {"database", qEnvironmentVariable("DB_MYSQL_DATABASE", "")}, + {"username", qEnvironmentVariable("DB_MYSQL_USERNAME", "root")}, + {"password", qEnvironmentVariable("DB_MYSQL_PASSWORD", "")}, + {"charset", qEnvironmentVariable("DB_MYSQL_CHARSET", "utf8mb4")}, + {"collation", qEnvironmentVariable("DB_MYSQL_COLLATION", + "utf8mb4_0900_ai_ci")}, + // Very important for tests + {"timezone", "+00:00"}, + {"prefix", ""}, + {"strict", true}, + {"options", QVariantHash()}, + // TODO future remove, when unit tested silverqx + // Example +// {"options", "MYSQL_OPT_CONNECT_TIMEOUT = 5 ; MYSQL_OPT_RECONNECT=1"}, +// {"options", QVariantHash {{"MYSQL_OPT_RECONNECT", 1}, +// {"MYSQL_OPT_READ_TIMEOUT", 10}}}, + }}, - static auto manager = DB::create({ - {"driver", "QMYSQL"}, - {"host", qEnvironmentVariable("DB_HOST", "127.0.0.1")}, - {"port", qEnvironmentVariable("DB_PORT", "3306")}, - {"database", qEnvironmentVariable("DB_DATABASE", "")}, - {"username", qEnvironmentVariable("DB_USERNAME", "root")}, - {"password", qEnvironmentVariable("DB_PASSWORD", "")}, - {"charset", qEnvironmentVariable("DB_CHARSET", "utf8mb4")}, - {"collation", qEnvironmentVariable("DB_COLLATION", "utf8mb4_unicode_ci")}, - {"prefix", ""}, - {"strict", true}, - {"options", QVariantHash()}, - // TODO future remove, when unit tested silverqx - // Example -// {"options", "MYSQL_OPT_CONNECT_TIMEOUT = 5 ; MYSQL_OPT_RECONNECT=1"}, -// {"options", QVariantHash {{"MYSQL_OPT_RECONNECT", 1}, -// {"MYSQL_OPT_READ_TIMEOUT", 10}}}, - }, connectionName); + {"tinyorm_sqlite_tests", { + {"driver", "QSQLITE"}, + {"database", qEnvironmentVariable("DB_SQLITE_DATABASE", + TINYORM_SQLITE_DATABASE)}, + {"foreign_key_constraints", qEnvironmentVariable("DB_SQLITE_FOREIGN_KEYS", + "true")}, + {"check_database_exists", true}, + }}, + }, + /* 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 auto &connection = manager->connection(connectionName); + static const auto cachedConnectionNames = manager->connectionNames(); - return connection; + return cachedConnectionNames; } } // namespace TestUtils diff --git a/tests/auto/utils/src/database.hpp b/tests/auto/utils/src/database.hpp index 9ee48b69b..d1df5fceb 100644 --- a/tests/auto/utils/src/database.hpp +++ b/tests/auto/utils/src/database.hpp @@ -10,10 +10,11 @@ namespace TestUtils class UTILS_EXPORT Database { - public: - Database(); + Q_DISABLE_COPY(Database) - static Orm::ConnectionInterface &createConnection(); + public: + /*! Create all database connections which will be tested. */ + static const QStringList &createConnections(); }; } diff --git a/tests/auto/utils/testdata/composer.json b/tests/auto/utils/testdata/composer.json new file mode 100644 index 000000000..5891521b8 --- /dev/null +++ b/tests/auto/utils/testdata/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "illuminate/database": "^8.33" + } +} diff --git a/tests/auto/utils/testdata/composer.lock b/tests/auto/utils/testdata/composer.lock new file mode 100644 index 000000000..2b67c4741 --- /dev/null +++ b/tests/auto/utils/testdata/composer.lock @@ -0,0 +1,1631 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a863f178b953497129b7de11f5c3ebdc", + "packages": [ + { + "name": "doctrine/inflector", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^7.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2020-05-29T15:13:26+00:00" + }, + { + "name": "illuminate/collections", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "d7cc717a00064b40fa63a8ad522042005e1de1ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/d7cc717a00064b40fa63a8ad522042005e1de1ed", + "reference": "d7cc717a00064b40fa63a8ad522042005e1de1ed", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "php": "^7.3|^8.0" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^5.1.4)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + }, + "files": [ + "helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-03-08T17:22:22+00:00" + }, + { + "name": "illuminate/container", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "6d7b2a3a3d430c27b90b6b336520e00e5c0f8354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/6d7b2a3a3d430c27b90b6b336520e00e5c0f8354", + "reference": "6d7b2a3a3d430c27b90b6b336520e00e5c0f8354", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0", + "php": "^7.3|^8.0", + "psr/container": "^1.0" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-03-11T03:45:11+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "121cea1d8b8772bc7fee99c71ecf0f57c1d77b3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/121cea1d8b8772bc7fee99c71ecf0f57c1d77b3b", + "reference": "121cea1d8b8772bc7fee99c71ecf0f57c1d77b3b", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0", + "psr/container": "^1.0", + "psr/simple-cache": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-03-12T14:45:30+00:00" + }, + { + "name": "illuminate/database", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "324924c914466424127d53c64ad297f04ef17aec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/324924c914466424127d53c64ad297f04ef17aec", + "reference": "324924c914466424127d53c64ad297f04ef17aec", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/collections": "^8.0", + "illuminate/container": "^8.0", + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "illuminate/support": "^8.0", + "php": "^7.3|^8.0", + "symfony/console": "^5.1.4" + }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "illuminate/console": "Required to use the database commands (^8.0).", + "illuminate/events": "Required to use the observers with Eloquent (^8.0).", + "illuminate/filesystem": "Required to use the migrations (^8.0).", + "illuminate/pagination": "Required to paginate the result set (^8.0).", + "symfony/finder": "Required to use Eloquent model factories (^5.1.4)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-03-10T15:18:48+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "300aa13c086f25116b5f3cde3ca54ff5c822fb05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/300aa13c086f25116b5f3cde3ca54ff5c822fb05", + "reference": "300aa13c086f25116b5f3cde3ca54ff5c822fb05", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2020-10-27T15:20:30+00:00" + }, + { + "name": "illuminate/support", + "version": "v8.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "520dcf5aa7631723fe6343afbeba777c102c98ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/520dcf5aa7631723fe6343afbeba777c102c98ae", + "reference": "520dcf5aa7631723fe6343afbeba777c102c98ae", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.4|^2.0", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/collections": "^8.0", + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "nesbot/carbon": "^2.31", + "php": "^7.3|^8.0", + "voku/portable-ascii": "^1.4.8" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "suggest": { + "illuminate/filesystem": "Required to use the composer class (^8.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^1.3).", + "ramsey/uuid": "Required to use Str::uuid() (^4.0).", + "symfony/process": "Required to use the composer class (^5.1.4).", + "symfony/var-dumper": "Required to use the dd function (^5.1.4).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + }, + "files": [ + "helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-03-15T13:43:13+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.46.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4", + "reference": "2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^3.4 || ^4.0 || ^5.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", + "kylekatarnls/multi-tester": "^2.0", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.54", + "phpunit/phpunit": "^7.5.20 || ^8.5.14", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-3.x": "3.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "kylekatarnls", + "homepage": "http://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2021-02-24T17:30:44+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "symfony/console", + "version": "v5.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79", + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-06T13:42:15+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/string", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4e78d7d47061fa183639927ec40d607973699609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4e78d7d47061fa183639927ec40d607973699609", + "reference": "4e78d7d47061fa183639927ec40d607973699609", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-16T10:20:28+00:00" + }, + { + "name": "symfony/translation", + "version": "v5.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "0947ab1e3aabd22a6bef393874b2555d2bb976da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/0947ab1e3aabd22a6bef393874b2555d2bb976da", + "reference": "0947ab1e3aabd22a6bef393874b2555d2bb976da", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.15", + "symfony/translation-contracts": "^2.3" + }, + "conflict": { + "symfony/config": "<4.4", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" + }, + "provide": { + "symfony/translation-implementation": "2.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/console": "^4.4|^5.0", + "symfony/dependency-injection": "^5.0", + "symfony/finder": "^4.4|^5.0", + "symfony/http-kernel": "^5.0", + "symfony/intl": "^4.4|^5.0", + "symfony/service-contracts": "^1.1.2|^2", + "symfony/yaml": "^4.4|^5.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v5.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-06T07:59:01+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105", + "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-28T13:05:58+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "80953678b19901e5165c56752d087fc11526017c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/80953678b19901e5165c56752d087fc11526017c", + "reference": "80953678b19901e5165c56752d087fc11526017c", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/1.5.6" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2020-11-12T00:07:28+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/tests/auto/utils/testdata/create_and_seed_database.php b/tests/auto/utils/testdata/create_and_seed_database.php new file mode 100644 index 000000000..91f40c1fa --- /dev/null +++ b/tests/auto/utils/testdata/create_and_seed_database.php @@ -0,0 +1,338 @@ +dropAllTables(); +} + +/** + * Add all configuration connections to the capsule. + * + * @param Capsule $capsule + * @param array $configs + */ +function addConnections(Capsule $capsule, array $configs = []) +{ + foreach ($configs as $name => $config) + $capsule->addConnection($config, $name); +} + +/** + * Create all tables for the given connection. + * + * @param string $connection The connection name + * @return void + */ +function createTables(string $connection) +{ + $schema = Capsule::schema($connection); + + $schema->create('settings', function (Blueprint $table) { + $table->string('name')->default('')->unique(); + $table->string('value')->default(''); + $table->timestamps(); + }); + + $schema->create('torrents', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique()->comment('Torrent name'); + $table->unsignedBigInteger('size')->default('0'); + $table->unsignedSmallInteger('progress')->default('0'); + $table->dateTime('added_on')->useCurrent(); + $table->string('hash', 40); + $table->string('note')->nullable(); + $table->timestamps(); + }); + + $schema->create('torrent_peers', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('torrent_id'); + $table->integer('seeds')->nullable(); + $table->integer('total_seeds')->nullable(); + $table->integer('leechers'); + $table->integer('total_leechers'); + $table->timestamps(); + + $table->foreign('torrent_id')->references('id')->on('torrents') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('torrent_previewable_files', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('torrent_id'); + $table->integer('file_index'); + $table->string('filepath')->unique(); + $table->unsignedBigInteger('size'); + $table->unsignedSmallInteger('progress'); + $table->string('note')->nullable(); + $table->timestamps(); + + $table->foreign('torrent_id')->references('id')->on('torrents') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('torrent_previewable_file_properties', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('previewable_file_id'); + $table->string('name')->unique(); + $table->unsignedBigInteger('size'); + $table->timestamps(); + + $table->foreign('previewable_file_id')->references('id') + ->on('torrent_previewable_files') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('file_property_properties', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('file_property_id'); + $table->string('name')->unique(); + $table->unsignedBigInteger('value'); + $table->timestamps(); + + $table->foreign('file_property_id')->references('id') + ->on('torrent_previewable_file_properties') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('torrent_tags', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + + $schema->create('tag_torrent', function (Blueprint $table) { + $table->unsignedBigInteger('torrent_id'); + $table->unsignedBigInteger('tag_id'); + $table->boolean('active')->default(1); + $table->timestamps(); + + $table->primary(['torrent_id', 'tag_id']); + + $table->foreign('torrent_id')->references('id')->on('torrents') + ->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreign('tag_id')->references('id')->on('torrent_tags') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('tag_properties', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('tag_id'); + $table->string('color'); + $table->unsignedInteger('position')->unique(); + $table->timestamps(); + + $table->foreign('tag_id')->references('id')->on('torrent_tags') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); + + $schema->create('users', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique(); + }); + + $schema->create('roles', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique(); + }); + + $schema->create('role_user', function (Blueprint $table) { + $table->unsignedBigInteger('role_id'); + $table->unsignedBigInteger('user_id'); + $table->boolean('active')->default(1); + + $table->primary(['role_id', 'user_id']); + + $table->foreign('role_id')->references('id')->on('roles') + ->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreign('user_id')->references('id')->on('users') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); +} + +/** + * Seed all tables with data. + * + * @param string $connection The connection name + * @return void + */ +function seedTables(string $connection) +{ + Capsule::table('torrents', null, $connection)->insert( + combineValues(['id', 'name', 'size', 'progress', 'added_on', 'hash', 'note', 'created_at', 'updated_at'], [ + [1, 'test1', 11, 100, '2020-08-01 20:11:10', '1579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-01 14:51:23', '2021-01-01 18:46:31'], + [2, 'test2', 12, 200, '2020-08-02 20:11:10', '2579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-02 14:51:23', '2021-01-02 18:46:31'], + [3, 'test3', 13, 300, '2020-08-03 20:11:10', '3579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-03 14:51:23', '2021-01-03 18:46:31'], + [4, 'test4', 14, 400, '2020-08-04 20:11:10', '4579e3af2768cdf52ec84c1f320333f68401dc6e', 'after update revert updated_at', '2021-01-04 14:51:23', '2021-01-04 18:46:31'], + [5, 'test5', 15, 500, '2020-08-05 20:11:10', '5579e3af2768cdf52ec84c1f320333f68401dc6e', 'no peers', '2021-01-05 14:51:23', '2021-01-05 18:46:31'], + [6, 'test6', 16, 600, '2020-08-06 20:11:10', '6579e3af2768cdf52ec84c1f320333f68401dc6e', 'no files no peers', '2021-01-06 14:51:23', '2021-01-06 18:46:31'], + ])); + + Capsule::table('torrent_peers', null, $connection)->insert( + combineValues(['id', 'torrent_id', 'seeds', 'total_seeds', 'leechers', 'total_leechers', 'created_at', 'updated_at'], [ + [1, 1, 1, 1, 1, 1, '2021-01-01 14:51:23', '2021-01-01 17:46:31'], + [2, 2, 2, 2, 2, 2, '2021-01-02 14:51:23', '2021-01-02 17:46:31'], + [3, 3, 3, 3, 3, 3, '2021-01-03 14:51:23', '2021-01-03 17:46:31'], + [4, 4, NULL, 4, 4, 4, '2021-01-04 14:51:23', '2021-01-04 17:46:31'], + ])); + + Capsule::table('torrent_previewable_files', null, $connection)->insert( + combineValues(['id', 'torrent_id', 'file_index', 'filepath', 'size', 'progress', 'note', 'created_at', 'updated_at'], [ + [1, 1, 0, 'test1_file1.mkv', 1024, 200, 'no file propreties', '2021-01-01 14:51:23', '2021-01-01 17:46:31'], + [2, 2, 0, 'test2_file1.mkv', 2048, 870, NULL, '2021-01-02 14:51:23', '2021-01-02 17:46:31'], + [3, 2, 1, 'test2_file2.mkv', 3072, 1000, NULL, '2021-01-02 14:51:23', '2021-01-02 17:46:31'], + [4, 3, 0, 'test3_file1.mkv', 5568, 870, NULL, '2021-01-03 14:51:23', '2021-01-03 17:46:31'], + [5, 4, 0, 'test4_file1.mkv', 4096, 0, NULL, '2021-01-04 14:51:23', '2021-01-04 17:46:31'], + [6, 5, 0, 'test5_file1.mkv', 2048, 999, NULL, '2021-01-05 14:51:23', '2021-01-05 17:46:31'], + [7, 5, 1, 'test5_file2.mkv', 2560, 890, 'for tst_BaseModel::remove()/destroy()', '2021-01-02 14:55:23', '2021-01-02 17:47:31'], + [8, 5, 2, 'test5_file3.mkv', 2570, 896, 'for tst_BaseModel::destroy()', '2021-01-02 14:56:23', '2021-01-02 17:48:31'], + ])); + + Capsule::table('torrent_previewable_file_properties', null, $connection)->insert( + combineValues(['id', 'previewable_file_id', 'name', 'size'], [ + [1, 2, 'test2_file1', 2], + [2, 3, 'test2_file2', 2], + [3, 4, 'test3_file1', 4], + [4, 5, 'test4_file1', 5], + [5, 6, 'test5_file1', 6], + ])); + + Capsule::table('file_property_properties', null, $connection)->insert( + combineValues(['id', 'file_property_id', 'name', 'value', 'created_at', 'updated_at'], [ + [1, 1, 'test2_file1_property1', 1, '2021-01-01 14:51:23', '2021-01-01 17:46:31'], + [2, 2, 'test2_file2_property1', 2, '2021-01-02 14:51:23', '2021-01-02 17:46:31'], + [3, 3, 'test3_file1_property1', 3, '2021-01-03 14:51:23', '2021-01-03 17:46:31'], + [4, 3, 'test3_file1_property2', 4, '2021-01-04 14:51:23', '2021-01-04 17:46:31'], + [5, 4, 'test4_file1_property1', 5, '2021-01-05 14:51:23', '2021-01-05 17:46:31'], + [6, 5, 'test5_file1_property1', 6, '2021-01-06 14:51:23', '2021-01-06 17:46:31'], + ])); + + Capsule::table('torrent_tags', null, $connection)->insert( + combineValues(['id', 'name', 'created_at', 'updated_at'], [ + [1, 'tag1', '2021-01-11 11:51:28', '2021-01-11 23:47:11'], + [2, 'tag2', '2021-01-12 11:51:28', '2021-01-12 23:47:11'], + [3, 'tag3', '2021-01-13 11:51:28', '2021-01-13 23:47:11'], + [4, 'tag4', '2021-01-14 11:51:28', '2021-01-14 23:47:11'], + ])); + + Capsule::table('tag_torrent', null, $connection)->insert( + combineValues(['torrent_id', 'tag_id', 'active', 'created_at', 'updated_at'], [ + [2, 1, 1, '2021-02-21 17:31:58', '2021-02-21 18:49:22'], + [2, 2, 1, '2021-02-22 17:31:58', '2021-02-22 18:49:22'], + [2, 3, 0, '2021-02-23 17:31:58', '2021-02-23 18:49:22'], + [2, 4, 1, '2021-02-24 17:31:58', '2021-02-24 18:49:22'], + [3, 2, 1, '2021-02-24 17:31:58', '2021-02-24 18:49:22'], + [3, 4, 1, '2021-02-24 17:31:58', '2021-02-24 18:49:22'], + ])); + + Capsule::table('tag_properties', null, $connection)->insert( + combineValues(['id', 'tag_id', 'color', 'position', 'created_at', 'updated_at'], [ + [1, 1, 'white', 0, '2021-02-11 12:41:28', '2021-02-11 22:17:11'], + [2, 2, 'blue', 1, '2021-02-12 12:41:28', '2021-02-12 22:17:11'], + [3, 3, 'red', 2, '2021-02-13 12:41:28', '2021-02-13 22:17:11'], + [4, 4, 'orange', 3, '2021-02-14 12:41:28', '2021-02-14 22:17:11'], + ])); + + Capsule::table('users', null, $connection)->insert( + combineValues(['id', 'name'], [ + [1, 'andrej'], + [2, 'silver'], + [3, 'peter'], + ])); + + Capsule::table('roles', null, $connection)->insert( + combineValues(['id', 'name'], [ + [1, 'role one'], + [2, 'role two'], + [3, 'role three'], + ])); + + Capsule::table('role_user', null, $connection)->insert( + combineValues(['role_id', 'user_id', 'active'], [ + [1, 1, 1], + [2, 1, 0], + [3, 1, 1], + [2, 2, 1], + ])); +} + +/** + * Create and seed all tables for all connections. + * + * @param array $connections Connection names + * @return void + */ +function createAndSeedTables(array $connections) +{ + foreach ($connections as $connection) { + dropAllTables($connection); + createTables($connection); + seedTables($connection); + } +} + +$capsule = new Capsule; + +$capsule->setAsGlobal(); +$capsule->bootEloquent(); + +$configs = [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => \getenv('DB_MYSQL_HOST') ?? '127.0.0.1', + 'port' => \getenv('DB_MYSQL_PORT') ?? '3306', + 'database' => \getenv('DB_MYSQL_DATABASE') ?? '', + 'username' => \getenv('DB_MYSQL_USERNAME') ?? 'root', + 'password' => \getenv('DB_MYSQL_PASSWORD') ?? '', + 'charset' => \getenv('DB_MYSQL_CHARSET') ?? 'utf8mb4', + 'collation' => \getenv('DB_MYSQL_COLLATION') ?? 'utf8mb4_0900_ai_ci', + 'timezone' => '+00:00', + 'prefix' => '', + ], + + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => \getenv('DB_SQLITE_DATABASE') ?? '', + 'prefix' => '', + 'foreign_key_constraints' => true, + ], +]; + +addConnections($capsule, $configs); +createAndSeedTables(array_keys($configs)); diff --git a/tests/auto/utils/testdata/mysql-q_tinyorm_test_1.sql b/tests/auto/utils/testdata/mysql-q_tinyorm_test_1.sql index ef4968a99..81b6fa7ef 100644 --- a/tests/auto/utils/testdata/mysql-q_tinyorm_test_1.sql +++ b/tests/auto/utils/testdata/mysql-q_tinyorm_test_1.sql @@ -70,12 +70,12 @@ CREATE TABLE `torrents` ( -- INSERT INTO `torrents` (`id`, `name`, `size`, `progress`, `added_on`, `hash`, `note`, `created_at`, `updated_at`) VALUES -(1, 'test1', 11, 100, '2020-08-01 20:11:10', '1579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-01 14:51:23', '2021-01-01 17:46:31'), -(2, 'test2', 12, 200, '2020-08-02 20:11:10', '2579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-02 14:51:23', '2021-01-02 17:46:31'), -(3, 'test3', 13, 300, '2020-08-03 20:11:10', '3579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-03 14:51:23', '2021-01-03 17:46:31'), -(4, 'test4', 14, 400, '2020-08-04 20:11:10', '4579e3af2768cdf52ec84c1f320333f68401dc6e', 'after update revert updated_at', '2021-01-04 14:51:23', '2021-01-04 17:46:31'), -(5, 'test5', 15, 500, '2020-08-05 20:11:10', '5579e3af2768cdf52ec84c1f320333f68401dc6e', 'no peers', '2021-01-05 14:51:23', '2021-01-05 17:46:31'), -(6, 'test6', 16, 600, '2020-08-06 20:11:10', '6579e3af2768cdf52ec84c1f320333f68401dc6e', 'no files no peers', '2021-01-06 14:51:23', '2021-01-06 17:46:31'); +(1, 'test1', 11, 100, '2020-08-01 20:11:10', '1579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-01 14:51:23', '2021-01-01 18:46:31'), +(2, 'test2', 12, 200, '2020-08-02 20:11:10', '2579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-02 14:51:23', '2021-01-02 18:46:31'), +(3, 'test3', 13, 300, '2020-08-03 20:11:10', '3579e3af2768cdf52ec84c1f320333f68401dc6e', NULL, '2021-01-03 14:51:23', '2021-01-03 18:46:31'), +(4, 'test4', 14, 400, '2020-08-04 20:11:10', '4579e3af2768cdf52ec84c1f320333f68401dc6e', 'after update revert updated_at', '2021-01-04 14:51:23', '2021-01-04 18:46:31'), +(5, 'test5', 15, 500, '2020-08-05 20:11:10', '5579e3af2768cdf52ec84c1f320333f68401dc6e', 'no peers', '2021-01-05 14:51:23', '2021-01-05 18:46:31'), +(6, 'test6', 16, 600, '2020-08-06 20:11:10', '6579e3af2768cdf52ec84c1f320333f68401dc6e', 'no files no peers', '2021-01-06 14:51:23', '2021-01-06 18:46:31'); -- -------------------------------------------------------- @@ -406,7 +406,7 @@ ALTER TABLE `settings` -- ALTER TABLE `torrents` ADD PRIMARY KEY (`id`), - ADD KEY `torrents_name_index` (`name`); + ADD KEY `torrents_name_unique` (`name`); -- -- Indexes for table `torrent_peers` @@ -480,7 +480,7 @@ ADD UNIQUE KEY `roles_name_unique` (`name`); -- ALTER TABLE `role_user` ADD PRIMARY KEY (`role_id`, `user_id`), - ADD KEY `role_user_role_id_foreign` (`role_id`); + ADD KEY `role_user_user_id_foreign` (`user_id`); -- -- AUTO_INCREMENT for dumped tables @@ -579,7 +579,7 @@ ALTER TABLE `tag_torrent` -- Constraints for table `tag_properties` -- ALTER TABLE `tag_properties` - ADD CONSTRAINT `torrent_tags_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `torrent_tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + ADD CONSTRAINT `tag_properties_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `torrent_tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; -- -- Constraints for table `role_user` diff --git a/tests/auto/utils/utils.pro b/tests/auto/utils/utils.pro index 69b271396..76a83abd0 100644 --- a/tests/auto/utils/utils.pro +++ b/tests/auto/utils/utils.pro @@ -73,3 +73,37 @@ include(src/src.pri) release { target.CONFIG += no_default_install } + +# Create the SQLite database +# --- + +build_tests { + # Default SQLite test database, can be overriden by DB_SQLITE_DATABASE env. variable + TINYORM_SQLITE_DATABASE = $$quote($$TINYORM_BUILD_TREE/tests/q_tinyorm_test_1.sqlite3) + + sqlitedatabase.target = sqlitedatabase + sqlitedatabase.dbname = $$TINYORM_SQLITE_DATABASE + sqlitedatabase.commands = touch $$sqlitedatabase.dbname + sqlitedatabase.depends = sqlitedatabase_message + + sqlitedatabase_message.commands = @echo Creating SQLite database at $$sqlitedatabase.dbname + + QMAKE_EXTRA_TARGETS += sqlitedatabase sqlitedatabase_message + + !exists($$TINYORM_SQLITE_DATABASE) { + POST_TARGETDEPS += sqlitedatabase + } + + # Set path to the SQLite database + # --- + contains(TEMPLATE, vc.*): DEFINES += TINYORM_SQLITE_DATABASE=\"$$TINYORM_SQLITE_DATABASE\" + else: DEFINES += TINYORM_SQLITE_DATABASE=$$shell_quote(\"$$TINYORM_SQLITE_DATABASE\") +} + +# Clean the SQLite database +# --- + +build_tests { + QMAKE_CLEAN = $$TINYORM_SQLITE_DATABASE + QMAKE_DISTCLEAN = $$TINYORM_SQLITE_DATABASE +} diff --git a/tests/config.pri b/tests/config.pri index 58beb25bd..defa0571d 100644 --- a/tests/config.pri +++ b/tests/config.pri @@ -18,6 +18,9 @@ CONFIG *= qt console testcase link_prl DEFINES += PROJECT_TINYORM_TEST +# Enable code needed by tests, eg connection overriding in the BaseModel +DEFINES += TINYORM_TESTS_CODE + # Use TinyORM's library precompiled headers (PCH) # ---