From 5ff160dec5fa1bebaaecab3a780d1c085593b82c Mon Sep 17 00:00:00 2001 From: silverqx Date: Fri, 12 Aug 2022 15:37:56 +0200 Subject: [PATCH] use upsert alias on MySQL The MySQL >=8.0.19 supports aliases in the VALUES and SET clauses of INSERT INTO ... ON DUPLICATE KEY UPDATE statement for the row to be inserted and its columns. It also generates warning from >=8.0.20 if an old style used. This enhancement detects the MySQL version and on the base of it uses alias during the upsert call. Also a user can override the version through the MySQL database configuration. It helps to save/avoid one database query (select version()) during the upsert method call or during connecting to the MySQL database (version is needed if strict mode is enabled). - added unit and functional tests - updated number of unit tests to 1402 - updated upsert docs - added ConfigUtils to avoid duplicates Others: - added the version database configuration everywhere - docs added few lines about MySQL version configuration option - docs updated database configurations, added a new missing options --- README.md | 3 +- cmake/Modules/TinySources.cmake | 2 + docs/building/migrations.mdx | 1 + docs/database/getting-started.mdx | 33 ++- docs/database/query-builder.mdx | 6 +- docs/features-summary.mdx | 2 +- docs/supported-compilers.mdx | 2 +- examples/tom/main.cpp | 1 + include/include.pri | 1 + include/orm/mysqlconnection.hpp | 20 +- include/orm/utils/configuration.hpp | 39 +++ src/orm/connectors/connectionfactory.cpp | 3 + src/orm/connectors/mysqlconnector.cpp | 11 +- src/orm/mysqlconnection.cpp | 98 +++++++- src/orm/query/grammars/mysqlgrammar.cpp | 25 +- src/orm/utils/configuration.cpp | 42 ++++ src/src.pri | 1 + tests/TinyUtils/src/databases.cpp | 2 + .../tst_mysql_querybuilder.cpp | 232 ++++++++++++++++-- tests/testdata_tom/main.cpp | 1 + 20 files changed, 479 insertions(+), 46 deletions(-) create mode 100644 include/orm/utils/configuration.hpp create mode 100644 src/orm/utils/configuration.cpp diff --git a/README.md b/README.md index 5d1ea36ad..6b2ce1223 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ Whole library is documented as markdown documents: - the `tom` console application with tab completion for all shells (pwsh, bash, zsh) đŸĨŗ - scaffolding of models, migrations, and seeders - overhauled models scaffolding, every feature that is supported by models can be generated using the `tom make:model` cli command -- a huge amount of code is unit tested, currently __1393 unit tests__ đŸ¤¯ +- a huge amount of code is unit tested, currently __1402 unit tests__ đŸ¤¯ - C++20 only, with all the latest features used like concepts/constraints, ranges, smart pointers (no `new` keyword in the whole code 😎), folding expressions - qmake and CMake build systems support + - CMake FetchContent module support 🤙 - vcpkg support (also the vcpkg port, currently not committed to the vcpkg repository â˜šī¸) - it's really fast, you can run 1000 complex queries in 500ms (heavily DB dependant, the PostgreSQL is by far the fastest) ⌚ - extensive documentation 📃 diff --git a/cmake/Modules/TinySources.cmake b/cmake/Modules/TinySources.cmake index 6a1c339a4..c6a62e025 100644 --- a/cmake/Modules/TinySources.cmake +++ b/cmake/Modules/TinySources.cmake @@ -97,6 +97,7 @@ function(tinyorm_sources out_headers out_sources) support/databaseconnectionsmap.hpp types/log.hpp types/statementscounter.hpp + utils/configuration.hpp utils/container.hpp utils/fs.hpp utils/helpers.hpp @@ -211,6 +212,7 @@ function(tinyorm_sources out_headers out_sources) schema/sqliteschemabuilder.cpp sqliteconnection.cpp support/configurationoptionsparser.cpp + utils/configuration.cpp utils/fs.cpp utils/query.cpp utils/thread.cpp diff --git a/docs/building/migrations.mdx b/docs/building/migrations.mdx index 7f2424eb2..187bb3b62 100644 --- a/docs/building/migrations.mdx +++ b/docs/building/migrations.mdx @@ -196,6 +196,7 @@ And paste the following code. {strict_, true}, {isolation_level, QStringLiteral("REPEATABLE READ")}, {engine_, InnoDB}, + {Version, {}}, // Autodetect {options_, QVariantHash()}, }, QStringLiteral("tinyorm_tom")); diff --git a/docs/database/getting-started.mdx b/docs/database/getting-started.mdx index 74494c083..ef9414ee9 100644 --- a/docs/database/getting-started.mdx +++ b/docs/database/getting-started.mdx @@ -39,17 +39,22 @@ You can create and configure new database connection by `create` method provided // Ownership of a shared_ptr() 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_0900_ai_ci")}, - {"timezone", "+00:00"}, - {"strict", true}, - {"options", QVariantHash()}, + {"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_0900_ai_ci")}, + {"timezone", "+00:00"}, + {"prefix", ""}, + {"strict", true}, + {"engine", InnoDB}, + {"version", {}}, // Autodetect + {"options", QVariantHash()}, + {"prefix_indexes", true}, + {"isolation_level", "REPEATABLE READ"}, }); The first argument is configuration hash which is of type `QVariantHash` and the second argument specifies the name of the *connection*, this connection will also be a *default connection*. You can configure multiple database connections at once and choose the needed one before executing SQL query, section [Using Multiple Database Connections](#using-multiple-database-connections) describes how to create and use multiple database connections. @@ -58,6 +63,10 @@ You may also configure connection options by `options` key as `QVariantHash` or You can also configure [Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html) for MySQL connection with the `isolation_level` configuration option. +The `version` option is relevant only for the MySQL connections and you can save/avoid one database query (select version()) if you provide it manually. On the base of this version will be decided which [session variables](https://github.com/silverqx/TinyORM/blob/main/src/orm/connectors/mysqlconnector.cpp#L183) will be set if strict mode is enabled and whether to use an [alias](https://github.com/silverqx/TinyORM/blob/main/src/orm/query/grammars/mysqlgrammar.cpp#L32) during the `upsert` method call. + +Breaking values are as follows; use an upsert alias on the MySQL >=8.0.19 and remove the `NO_AUTO_CREATE_USER` sql mode on the MySQL >=8.0.11 if the strict mode is enabled. + :::info A database connection is resolved lazily, which means that the connection configuration is only saved after the `DB::create` method call. The connection will be resolved after you run some query or you can create it using the `DB::connection` method. ::: @@ -78,6 +87,7 @@ SQLite databases are contained within a single file on your filesystem. You can {"database", qEnvironmentVariable("DB_DATABASE", "/absolute/path/to/database.sqlite3")}, {"foreign_key_constraints", qEnvironmentVariable("DB_FOREIGN_KEYS", "true")}, {"check_database_exists", true}, + {"prefix", ""}, }); The `database` configuration value is the absolute path to the database. To enable foreign key constraints for SQLite connections, you should set the `foreign_key_constraints` configuration value to `true`, if this configuration value is not set, then the default of the SQLite driver will be used. @@ -196,6 +206,7 @@ You can configure multiple database connections at once during `DatabaseManager` {"database", qEnvironmentVariable("DB_SQLITE_DATABASE", "")}, {"foreign_key_constraints", qEnvironmentVariable("DB_SQLITE_FOREIGN_KEYS", "true")}, {"check_database_exists", true}, + {"prefix", ""}, }}, }, "mysql"); diff --git a/docs/database/query-builder.mdx b/docs/database/query-builder.mdx index f14e4bce0..2de13d617 100644 --- a/docs/database/query-builder.mdx +++ b/docs/database/query-builder.mdx @@ -761,10 +761,14 @@ The `upsert` method will insert records that do not exist and update the records In the example above, TinyORM will attempt to insert two records. If a record already exists with the same `departure` and `destination` column values, Laravel will update that record's `price` column. -:::warning +:::caution All databases except SQL Server require the columns in the second argument of the `upsert` method to have a "primary" or "unique" index. In addition, the MySQL database driver ignores the second argument of the `upsert` method and always uses the "primary" and "unique" indexes of the table to detect existing records. ::: +:::info +Row and column aliases will be used with MySQL server >=8.0.19 instead of the VALUES() function as is described in the MySQL [documentation](https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html). The MySQL server version is auto-detected and can be overridden in the [configuration](/database/getting-started.mdx#configuration). +::: + ## Update Statements In addition to inserting records into the database, the query builder can also update existing records using the `update` method. The `update` method, accepts a `QVector` of column and value pairs, indicating the columns to be updated and returns a `std::tuple` . You may constrain the `update` query using `where` clauses: diff --git a/docs/features-summary.mdx b/docs/features-summary.mdx index 53789aada..7fe86d45c 100644 --- a/docs/features-summary.mdx +++ b/docs/features-summary.mdx @@ -46,7 +46,7 @@ The following list fastly summarizes all `TinyORM` features. - the `tom` console application with tab completion for all shells (pwsh, bash, zsh) đŸĨŗ - scaffolding of models, migrations, and seeders - overhauled models scaffolding, every feature that is supported by models can be generated using the `tom make:model` cli command -- a huge amount of code is unit tested, currently __1393 unit tests__ đŸ¤¯ +- a huge amount of code is unit tested, currently __1402 unit tests__ đŸ¤¯ - C++20 only, with all the latest features used like concepts/constraints, ranges, smart pointers (no `new` keyword in the whole code 😎), folding expressions - qmake and CMake build systems support - vcpkg support (also the vcpkg port, currently not committed to the vcpkg repository â˜šī¸) diff --git a/docs/supported-compilers.mdx b/docs/supported-compilers.mdx index dc2d2be8b..07a77a97a 100644 --- a/docs/supported-compilers.mdx +++ b/docs/supported-compilers.mdx @@ -8,7 +8,7 @@ keywords: [c++ orm, supported compilers, supported build systems, tinyorm] # Supported Compilers -Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then __1393 unit tests__ 😮đŸ’Ĩ. +Following compilers are backed up by the GitHub Action [workflows](https://github.com/silverqx/TinyORM/tree/main/.github/workflows) (CI pipelines), these workflows also include more then __1402 unit tests__ 😮đŸ’Ĩ.
diff --git a/examples/tom/main.cpp b/examples/tom/main.cpp index f633e706b..d9347c161 100644 --- a/examples/tom/main.cpp +++ b/examples/tom/main.cpp @@ -83,6 +83,7 @@ std::shared_ptr setupManager() {strict_, true}, {isolation_level, QStringLiteral("REPEATABLE READ")}, {engine_, InnoDB}, + {Version, {}}, // Autodetect {options_, QVariantHash()}, }}, diff --git a/include/include.pri b/include/include.pri index 7c9e91c19..5a69d571a 100644 --- a/include/include.pri +++ b/include/include.pri @@ -89,6 +89,7 @@ headersList += \ $$PWD/orm/support/databaseconnectionsmap.hpp \ $$PWD/orm/types/log.hpp \ $$PWD/orm/types/statementscounter.hpp \ + $$PWD/orm/utils/configuration.hpp \ $$PWD/orm/utils/container.hpp \ $$PWD/orm/utils/fs.hpp \ $$PWD/orm/utils/helpers.hpp \ diff --git a/include/orm/mysqlconnection.hpp b/include/orm/mysqlconnection.hpp index 969d0662e..53610a858 100644 --- a/include/orm/mysqlconnection.hpp +++ b/include/orm/mysqlconnection.hpp @@ -29,9 +29,17 @@ namespace Orm /*! Get a schema builder instance for the connection. */ std::unique_ptr getSchemaBuilder() final; - /* Getters */ - /*! Determine if the connected database is a MariaDB database. */ + /* Getters/Setters */ + /*! Get the MySQL server version. */ + std::optional version(); + /*! Is currently connected the MariaDB database server? */ bool isMaria(); + /*! Determine whether to use the upsert alias (by MySQL version >=8.0.19). */ + bool useUpsertAlias(); +#ifdef TINYORM_TESTS_CODE + /*! Override the version database configuration value. */ + void setConfigVersion(QString value); +#endif /* Others */ /*! Check database connection and show warnings when the state changed. @@ -47,8 +55,12 @@ namespace Orm /*! Get the default post processor instance. */ std::unique_ptr getDefaultPostProcessor() const final; - /*! If the connected database is a MariaDB database. */ - std::optional m_isMaria; + /*! MySQL server version. */ + std::optional m_version = std::nullopt; + /*! Is currently connected the MariaDB database server? */ + std::optional m_isMaria = std::nullopt; + /*! Determine whether to use the upsert alias (by MySQL version >=8.0.19). */ + std::optional m_useUpsertAlias = std::nullopt; }; } // namespace Orm diff --git a/include/orm/utils/configuration.hpp b/include/orm/utils/configuration.hpp new file mode 100644 index 000000000..da8163bc4 --- /dev/null +++ b/include/orm/utils/configuration.hpp @@ -0,0 +1,39 @@ +#pragma once +#ifndef ORM_UTILS_CONFIG_HPP +#define ORM_UTILS_CONFIG_HPP + +#include "orm/macros/systemheader.hpp" +TINY_SYSTEM_HEADER + +#include + +#include "orm/macros/commonnamespace.hpp" +#include "orm/macros/export.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm::Utils +{ + + /*! Database configuration related library class. */ + class SHAREDLIB_EXPORT Configuration + { + Q_DISABLE_COPY(Configuration) + + public: + /*! Deleted default constructor, this is a pure library class. */ + Configuration() = delete; + /*! Deleted destructor. */ + ~Configuration() = delete; + + /*! Determine whether the database config. contains a valid version value. */ + static bool hasValidConfigVersion(const QVariantHash &config); + /*! Get a valid config. version value. */ + static QString getValidConfigVersion(const QVariantHash &config); + }; + +} // namespace Orm::Utils + +TINYORM_END_COMMON_NAMESPACE + +#endif // ORM_UTILS_CONFIG_HPP diff --git a/src/orm/connectors/connectionfactory.cpp b/src/orm/connectors/connectionfactory.cpp index 37aaa0685..af34f9b42 100644 --- a/src/orm/connectors/connectionfactory.cpp +++ b/src/orm/connectors/connectionfactory.cpp @@ -75,6 +75,9 @@ ConnectionFactory::parseConfig(QVariantHash &config, const QString &name) const // spatial_ref_sys table is used by the PostGIS config.insert(dont_drop, QStringList {QStringLiteral("spatial_ref_sys")}); + if (config[driver_] == QMYSQL && !config.contains(Version)) + config.insert(Version, {}); + return config; } diff --git a/src/orm/connectors/mysqlconnector.cpp b/src/orm/connectors/mysqlconnector.cpp index 3a6d3c72c..55a9c2ae9 100644 --- a/src/orm/connectors/mysqlconnector.cpp +++ b/src/orm/connectors/mysqlconnector.cpp @@ -5,6 +5,7 @@ #include "orm/constants.hpp" #include "orm/exceptions/queryerror.hpp" +#include "orm/utils/configuration.hpp" #include "orm/utils/type.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -17,6 +18,8 @@ using Orm::Constants::NAME; using Orm::Constants::strict_; using Orm::Constants::timezone_; +using ConfigUtils = Orm::Utils::Configuration; + namespace Orm::Connectors { @@ -194,9 +197,11 @@ QString MySqlConnector::strictMode(const QSqlDatabase &connection, QString MySqlConnector::getMySqlVersion(const QSqlDatabase &connection, const QVariantHash &config) const { - // Get the MySQL version from the configuration if it was defined - if (config.contains("version") && !config["version"].value().isEmpty()) - return config["version"].value(); + // Get the MySQL version from the configuration if it was defined and is valid + if (auto configVersionValue = ConfigUtils::getValidConfigVersion(config); + !configVersionValue.isEmpty() + ) + return configVersionValue; // Obtain the MySQL version from the database QSqlQuery query(connection); diff --git a/src/orm/mysqlconnection.cpp b/src/orm/mysqlconnection.cpp index 37c2052fa..8cb859952 100644 --- a/src/orm/mysqlconnection.cpp +++ b/src/orm/mysqlconnection.cpp @@ -3,6 +3,7 @@ #ifdef TINYORM_MYSQL_PING # include #endif +#include #include #ifdef TINYORM_MYSQL_PING @@ -23,9 +24,12 @@ disable TINYORM_MYSQL_PING preprocessor directive. #include "orm/query/processors/mysqlprocessor.hpp" #include "orm/schema/grammars/mysqlschemagrammar.hpp" #include "orm/schema/mysqlschemabuilder.hpp" +#include "orm/utils/configuration.hpp" TINYORM_BEGIN_COMMON_NAMESPACE +using ConfigUtils = Orm::Utils::Configuration; + namespace Orm { @@ -55,18 +59,100 @@ std::unique_ptr MySqlConnection::getSchemaBuilder() return std::make_unique(*this); } -/* Getters */ +/* Getters/Setters */ + +std::optional MySqlConnection::version() +{ + auto configVersionValue = ConfigUtils::getValidConfigVersion(m_config); + + /* Default values is std::nullopt if pretending and the database config. doesn't + contain a valid version value. */ + if (m_pretending && !m_version && configVersionValue.isEmpty()) + return std::nullopt; + + // Return the cached value + if (m_version) + return m_version; + + // A user can provide the version through the configuration to save one DB query + if (!configVersionValue.isEmpty()) + return m_version = std::move(configVersionValue); + + // Obtain and cache the database version value + return m_version = selectOne(QStringLiteral("select version()")).value(0) + .value(); +} bool MySqlConnection::isMaria() { - // TEST now add MariaDB tests, install mariadb add connection and run all the tests against mariadb too silverqx - if (!m_isMaria) - m_isMaria = selectOne("select version()").value(0).value() - .contains("MariaDB"); + // Default values is false if pretending and the config. version was not set manually + if (m_pretending && !m_isMaria && !m_version && + !ConfigUtils::hasValidConfigVersion(m_config) + ) + return false; + + // Return the cached value + if (m_isMaria) + return *m_isMaria; + + // Obtain a version from the database if needed + version(); + + // This should never happen 🤔 because of the condition at beginning + if (!m_version) + return false; + + // Cache the value + m_isMaria = m_version->contains(QStringLiteral("MariaDB")); return *m_isMaria; } +bool MySqlConnection::useUpsertAlias() +{ + // Default values is true if pretending and the config. version was not set manually + if (m_pretending && !m_useUpsertAlias && !m_version && + !ConfigUtils::hasValidConfigVersion(m_config) + ) + // FUTURE useUpsertAlias() default value to true after MySQL 8.0 will be end-of-life silverqx + return false; + + // Return the cached value + if (m_useUpsertAlias) + return *m_useUpsertAlias; + + // Obtain a version from the database if needed + version(); + + // This should never happen 🤔 because of the condition at beginning + if (!m_version) + return false; + + /* The MySQL >=8.0.19 supports aliases in the VALUES and SET clauses + of INSERT INTO ... ON DUPLICATE KEY UPDATE statement for the row to be + inserted and its columns. It also generates warning if old style used. + So set it to true to avoid this warning. */ + + // Cache the value + m_useUpsertAlias = QVersionNumber::fromString(*m_version) >= + QVersionNumber(8, 0, 19); + + return *m_useUpsertAlias; +} + +#ifdef TINYORM_TESTS_CODE +void MySqlConnection::setConfigVersion(QString value) // NOLINT(performance-unnecessary-value-param) +{ + // Override it through the config., this ensure that more code branches will be tested + const_cast(m_config).insert(Version, std::move(value)); + + // We need to reset these to recomputed them again + m_version = std::nullopt; + m_isMaria = std::nullopt; + m_useUpsertAlias = std::nullopt; +} +#endif + /* Others */ bool MySqlConnection::pingDatabase() @@ -169,3 +255,5 @@ std::unique_ptr MySqlConnection::getDefaultPostProcessor() const } // namespace Orm TINYORM_END_COMMON_NAMESPACE + +// TEST now add MariaDB tests, install mariadb add connection and run all the tests against mariadb too silverqx diff --git a/src/orm/query/grammars/mysqlgrammar.cpp b/src/orm/query/grammars/mysqlgrammar.cpp index 70a98cf52..b2f841b35 100644 --- a/src/orm/query/grammars/mysqlgrammar.cpp +++ b/src/orm/query/grammars/mysqlgrammar.cpp @@ -1,6 +1,7 @@ #include "orm/query/grammars/mysqlgrammar.hpp" #include "orm/macros/threadlocal.hpp" +#include "orm/mysqlconnection.hpp" #include "orm/query/querybuilder.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -25,21 +26,35 @@ QString MySqlGrammar::compileInsertOrIgnore(const QueryBuilder &query, } QString MySqlGrammar::compileUpsert( - QueryBuilder &query, const QVector &values, - const QStringList &/*unused*/, const QStringList &update) const + QueryBuilder &query, const QVector &values, + const QStringList &/*unused*/, const QStringList &update) const { + static const auto TinyOrmUpsertAlias = QStringLiteral("tinyorm_upsert_alias"); + + // Use an upsert alias on the MySQL >=8.0.19 + const auto useUpsertAlias = dynamic_cast(query.getConnection()) + .useUpsertAlias(); + auto sql = compileInsert(query, values); + if (useUpsertAlias) + sql += QStringLiteral(" as %1") + .arg(wrap(QStringLiteral("tinyorm_upsert_alias"))); + sql += QStringLiteral(" on duplicate key update "); QStringList columns; columns.reserve(update.size()); for (const auto &column : update) { - auto wrappedColumn = wrap(column); + const auto wrappedColumn = wrap(column); - columns << QStringLiteral("%1 = values(%2)") - .arg(wrappedColumn, std::move(wrappedColumn)); + columns << (useUpsertAlias ? QStringLiteral("%1 = %2") + .arg(wrappedColumn, + DOT_IN.arg(wrap(TinyOrmUpsertAlias), + wrappedColumn)) + : QStringLiteral("%1 = values(%2)") + .arg(wrappedColumn, wrappedColumn)); } return NOSPACE.arg(std::move(sql), columns.join(COMMA)); diff --git a/src/orm/utils/configuration.cpp b/src/orm/utils/configuration.cpp new file mode 100644 index 000000000..3d5ab1634 --- /dev/null +++ b/src/orm/utils/configuration.cpp @@ -0,0 +1,42 @@ +#include "orm/utils/configuration.hpp" + +#include +#include + +#include "orm/constants.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +using Orm::Constants::Version; + +namespace Orm::Utils +{ + +/* public */ + +bool Configuration::hasValidConfigVersion(const QVariantHash &config) +{ + return !getValidConfigVersion(config).isEmpty(); +} + +QString Configuration::getValidConfigVersion(const QVariantHash &config) +{ + if (config.contains(Version)) + if (const auto version = config.value(Version); + version.isValid() && !version.isNull() && version.canConvert() + ) { + auto versionValue = version.value(); + + // Validate whether a version number is correctly formatted + if (const auto versionNumber = QVersionNumber::fromString(versionValue); + !versionNumber.isNull() + ) + return versionValue; + } + + return {}; +} + +} // namespace Orm::Utils + +TINYORM_END_COMMON_NAMESPACE diff --git a/src/src.pri b/src/src.pri index 93fae4c53..ed886b9e6 100644 --- a/src/src.pri +++ b/src/src.pri @@ -51,6 +51,7 @@ sourcesList += \ $$PWD/orm/schema/sqliteschemabuilder.cpp \ $$PWD/orm/sqliteconnection.cpp \ $$PWD/orm/support/configurationoptionsparser.cpp \ + $$PWD/orm/utils/configuration.cpp \ $$PWD/orm/utils/fs.cpp \ $$PWD/orm/utils/query.cpp \ $$PWD/orm/utils/thread.cpp \ diff --git a/tests/TinyUtils/src/databases.cpp b/tests/TinyUtils/src/databases.cpp index b6b4b4c4a..738045be2 100644 --- a/tests/TinyUtils/src/databases.cpp +++ b/tests/TinyUtils/src/databases.cpp @@ -20,6 +20,7 @@ using Orm::Constants::UTC; using Orm::Constants::UTF8; using Orm::Constants::UTF8MB4; using Orm::Constants::UTF8MB40900aici; +using Orm::Constants::Version; using Orm::Constants::database_; using Orm::Constants::driver_; using Orm::Constants::charset_; @@ -171,6 +172,7 @@ Databases::mysqlConfiguration() {strict_, true}, {isolation_level, QStringLiteral("REPEATABLE READ")}, {engine_, InnoDB}, + {Version, {}}, // Autodetect {options_, QVariantHash()}, // FUTURE remove, when unit tested silverqx // Example diff --git a/tests/auto/unit/orm/query/mysql_querybuilder/tst_mysql_querybuilder.cpp b/tests/auto/unit/orm/query/mysql_querybuilder/tst_mysql_querybuilder.cpp index 640c6d8a4..45047cd4e 100644 --- a/tests/auto/unit/orm/query/mysql_querybuilder/tst_mysql_querybuilder.cpp +++ b/tests/auto/unit/orm/query/mysql_querybuilder/tst_mysql_querybuilder.cpp @@ -5,6 +5,7 @@ #include "orm/exceptions/invalidargumenterror.hpp" #include "orm/exceptions/multiplerecordsfounderror.hpp" #include "orm/exceptions/recordsnotfounderror.hpp" +#include "orm/mysqlconnection.hpp" #include "orm/query/querybuilder.hpp" #include "databases.hpp" @@ -26,6 +27,7 @@ using Orm::DB; using Orm::Exceptions::InvalidArgumentError; using Orm::Exceptions::MultipleRecordsFoundError; using Orm::Exceptions::RecordsNotFoundError; +using Orm::MySqlConnection; using Orm::Query::Builder; using Orm::Query::Expression; @@ -60,6 +62,14 @@ class tst_MySql_QueryBuilder : public QObject // clazy:exclude=ctor-missing-pare private Q_SLOTS: void initTestCase(); + void version() const; + void version_InPretend() const; + void version_InPretend_DefaultValue() const; + + void isMaria() const; + void isMaria_InPretend() const; + void isMaria_InPretend_DefaultValue() const; + void get() const; void get_ColumnExpression() const; @@ -192,8 +202,11 @@ private Q_SLOTS: void update() const; void update_WithExpression() const; - void upsert() const; - void upsert_WithoutUpdate_UpdateAll() const; + void upsert_UseUpsertAlias() const; + void upsert_UseUpsertAlias_Disabled() const; + void upsert_UseUpsertAlias_DefaultValue() const; + void upsert_WithoutUpdate_UpdateAll_UseUpsertAlias() const; + void upsert_WithoutUpdate_UpdateAll_UseUpsertAlias_Disabled() const; void remove() const; void remove_WithExpression() const; @@ -232,6 +245,84 @@ void tst_MySql_QueryBuilder::initTestCase() .arg("tst_MySql_QueryBuilder", Databases::MYSQL).toUtf8().constData(), ); } +void tst_MySql_QueryBuilder::version() const +{ + auto version = QStringLiteral("10.8.3-MariaDB"); + + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion(version); + + QCOMPARE(mysqlConnection.version(), version); +} + +void tst_MySql_QueryBuilder::version_InPretend() const +{ + auto version = QStringLiteral("10.8.3-MariaDB"); + + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion(version); + + auto log = mysqlConnection.pretend([&mysqlConnection, &version] + { + QCOMPARE(mysqlConnection.version(), version); + }); + + QVERIFY(log.isEmpty()); +} + +void tst_MySql_QueryBuilder::version_InPretend_DefaultValue() const +{ + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion({}); + + auto log = mysqlConnection.pretend([&mysqlConnection] + { + // No version set so it should return std::nullopt + QVERIFY(!mysqlConnection.version()); + }); + + QVERIFY(log.isEmpty()); +} + +void tst_MySql_QueryBuilder::isMaria() const +{ + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("10.4.7-MariaDB"); + + QVERIFY(mysqlConnection.isMaria()); +} + +void tst_MySql_QueryBuilder::isMaria_InPretend() const +{ + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("10.4.7-MariaDB"); + + auto log = mysqlConnection.pretend([&mysqlConnection] + { + QVERIFY(mysqlConnection.isMaria()); + }); + + QVERIFY(log.isEmpty()); +} + +void tst_MySql_QueryBuilder::isMaria_InPretend_DefaultValue() const +{ + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion({}); + + auto log = mysqlConnection.pretend([&mysqlConnection] + { + // No version set so it the default value is false + QVERIFY(!mysqlConnection.isMaria()); + }); + + QVERIFY(log.isEmpty()); +} + void tst_MySql_QueryBuilder::get() const { { @@ -2768,9 +2859,13 @@ void tst_MySql_QueryBuilder::update_WithExpression() const QVector({QVariant(6), QVariant(10)})); } -void tst_MySql_QueryBuilder::upsert() const +void tst_MySql_QueryBuilder::upsert_UseUpsertAlias() const { - auto log = DB::connection(m_connection).pretend([](auto &connection) + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("8.0.19"); + + auto log = mysqlConnection.pretend([](auto &connection) { connection.query()->from("tag_properties") .upsert({{{"tag_id", 1}, {"color", "pink"}, {"position", 0}}, @@ -2779,22 +2874,92 @@ void tst_MySql_QueryBuilder::upsert() const {"color"}); }); - QVERIFY(!log.isEmpty()); - const auto &firstLog = log.first(); + // MySQL >=8.0.19 uses upsert alias + const auto useUpsertAlias = mysqlConnection.useUpsertAlias(); + QVERIFY(useUpsertAlias); QCOMPARE(log.size(), 1); - QCOMPARE(firstLog.query, + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, "insert into `tag_properties` (`color`, `position`, `tag_id`) " - "values (?, ?, ?), (?, ?, ?) " - "on duplicate key update `color` = values(`color`)"); - QCOMPARE(firstLog.boundValues, + "values (?, ?, ?), (?, ?, ?) as `tinyorm_upsert_alias` " + "on duplicate key update " + "`color` = `tinyorm_upsert_alias`.`color`"); + QCOMPARE(log0.boundValues, QVector({QVariant(QString("pink")), QVariant(0), QVariant(1), QVariant(QString("purple")), QVariant(4), QVariant(1)})); } -void tst_MySql_QueryBuilder::upsert_WithoutUpdate_UpdateAll() const +void tst_MySql_QueryBuilder::upsert_UseUpsertAlias_Disabled() const { - auto log = DB::connection(m_connection).pretend([](auto &connection) + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("8.0.18"); + + auto log = mysqlConnection.pretend([](auto &connection) + { + connection.query()->from("tag_properties") + .upsert({{{"tag_id", 1}, {"color", "pink"}, {"position", 0}}, + {{"tag_id", 1}, {"color", "purple"}, {"position", 4}}}, + {"position"}, + {"color"}); + }); + + // MySQL <8.0.19 doesn't use upsert alias + const auto useUpsertAlias = mysqlConnection.useUpsertAlias(); + QVERIFY(!useUpsertAlias); + + QCOMPARE(log.size(), 1); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "insert into `tag_properties` (`color`, `position`, `tag_id`) " + "values (?, ?, ?), (?, ?, ?) " + "on duplicate key update `color` = values(`color`)"); + QCOMPARE(log0.boundValues, + QVector({QVariant(QString("pink")), QVariant(0), QVariant(1), + QVariant(QString("purple")), QVariant(4), QVariant(1)})); +} + +void tst_MySql_QueryBuilder::upsert_UseUpsertAlias_DefaultValue() const +{ + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion({}); + + auto log = mysqlConnection.pretend([&mysqlConnection](auto &connection) + { + connection.query()->from("tag_properties") + .upsert({{{"tag_id", 1}, {"color", "pink"}, {"position", 0}}, + {{"tag_id", 1}, {"color", "purple"}, {"position", 4}}}, + {"position"}, + {"color"}); + + /* Default value for the use upsert alias feature during pretending will be false + because no version was provided through the database configuration. */ + const auto useUpsertAlias = mysqlConnection.useUpsertAlias(); + QVERIFY(!useUpsertAlias); + }); + + QCOMPARE(log.size(), 1); + + const auto &log0 = log.at(0); + QCOMPARE(log0.query, + "insert into `tag_properties` (`color`, `position`, `tag_id`) " + "values (?, ?, ?), (?, ?, ?) " + "on duplicate key update `color` = values(`color`)"); + QCOMPARE(log0.boundValues, + QVector({QVariant(QString("pink")), QVariant(0), QVariant(1), + QVariant(QString("purple")), QVariant(4), QVariant(1)})); +} + +void tst_MySql_QueryBuilder::upsert_WithoutUpdate_UpdateAll_UseUpsertAlias() const +{ + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("8.0.19"); + + auto log = mysqlConnection.pretend([](auto &connection) { connection.query()->from("tag_properties") .upsert({{{"tag_id", 2}, {"color", "pink"}, {"position", 0}}, @@ -2802,6 +2967,45 @@ void tst_MySql_QueryBuilder::upsert_WithoutUpdate_UpdateAll() const {"position"}); }); + // MySQL >=8.0.19 uses upsert alias + const auto useUpsertAlias = mysqlConnection.useUpsertAlias(); + QVERIFY(useUpsertAlias); + + QVERIFY(!log.isEmpty()); + const auto &firstLog = log.first(); + + QCOMPARE(log.size(), 1); + QCOMPARE(firstLog.query, + "insert into `tag_properties` (`color`, `position`, `tag_id`) " + "values (?, ?, ?), (?, ?, ?) as `tinyorm_upsert_alias` " + "on duplicate key update " + "`color` = `tinyorm_upsert_alias`.`color`, " + "`position` = `tinyorm_upsert_alias`.`position`, " + "`tag_id` = `tinyorm_upsert_alias`.`tag_id`"); + QCOMPARE(firstLog.boundValues, + QVector({QVariant(QString("pink")), QVariant(0), QVariant(2), + QVariant(QString("purple")), QVariant(4), QVariant(1)})); +} + +void tst_MySql_QueryBuilder +::upsert_WithoutUpdate_UpdateAll_UseUpsertAlias_Disabled() const +{ + // Need to be set before pretending + auto &mysqlConnection = dynamic_cast(DB::connection(m_connection)); + mysqlConnection.setConfigVersion("8.0.18"); + + auto log = mysqlConnection.pretend([](auto &connection) + { + connection.query()->from("tag_properties") + .upsert({{{"tag_id", 2}, {"color", "pink"}, {"position", 0}}, + {{"tag_id", 1}, {"color", "purple"}, {"position", 4}}}, + {"position"}); + }); + + // MySQL <8.0.19 doesn't use upsert alias + const auto useUpsertAlias = mysqlConnection.useUpsertAlias(); + QVERIFY(!useUpsertAlias); + QVERIFY(!log.isEmpty()); const auto &firstLog = log.first(); @@ -2810,8 +3014,8 @@ void tst_MySql_QueryBuilder::upsert_WithoutUpdate_UpdateAll() const "insert into `tag_properties` (`color`, `position`, `tag_id`) " "values (?, ?, ?), (?, ?, ?) " "on duplicate key update `color` = values(`color`), " - "`position` = values(`position`), " - "`tag_id` = values(`tag_id`)"); + "`position` = values(`position`), " + "`tag_id` = values(`tag_id`)"); QCOMPARE(firstLog.boundValues, QVector({QVariant(QString("pink")), QVariant(0), QVariant(2), QVariant(QString("purple")), QVariant(4), QVariant(1)})); diff --git a/tests/testdata_tom/main.cpp b/tests/testdata_tom/main.cpp index 0ee05e32e..45b3ddd0e 100644 --- a/tests/testdata_tom/main.cpp +++ b/tests/testdata_tom/main.cpp @@ -86,6 +86,7 @@ std::shared_ptr setupManager() {strict_, true}, {isolation_level, QStringLiteral("REPEATABLE READ")}, {engine_, InnoDB}, + {Version, {}}, // Autodetect {options_, QVariantHash()}, }},