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
This commit is contained in:
silverqx
2022-08-12 15:37:56 +02:00
parent d8a024a937
commit 5ff160dec5
20 changed files with 479 additions and 46 deletions

View File

@@ -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 📃

View File

@@ -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

View File

@@ -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"));

View File

@@ -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");

View File

@@ -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<Orm::UpdateItem>` of column and value pairs, indicating the columns to be updated and returns a `std::tuple<int, QSqlQuery>` . You may constrain the `update` query using `where` clauses:

View File

@@ -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 ☹️)

View File

@@ -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__ 😮💥.
<div id="supported-compilers">

View File

@@ -83,6 +83,7 @@ std::shared_ptr<DatabaseManager> setupManager()
{strict_, true},
{isolation_level, QStringLiteral("REPEATABLE READ")},
{engine_, InnoDB},
{Version, {}}, // Autodetect
{options_, QVariantHash()},
}},

View File

@@ -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 \

View File

@@ -29,9 +29,17 @@ namespace Orm
/*! Get a schema builder instance for the connection. */
std::unique_ptr<SchemaBuilder> getSchemaBuilder() final;
/* Getters */
/*! Determine if the connected database is a MariaDB database. */
/* Getters/Setters */
/*! Get the MySQL server version. */
std::optional<QString> 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<QueryProcessor> getDefaultPostProcessor() const final;
/*! If the connected database is a MariaDB database. */
std::optional<bool> m_isMaria;
/*! MySQL server version. */
std::optional<QString> m_version = std::nullopt;
/*! Is currently connected the MariaDB database server? */
std::optional<bool> m_isMaria = std::nullopt;
/*! Determine whether to use the upsert alias (by MySQL version >=8.0.19). */
std::optional<bool> m_useUpsertAlias = std::nullopt;
};
} // namespace Orm

View File

@@ -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 <QtGlobal>
#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

View File

@@ -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;
}

View File

@@ -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<QString>().isEmpty())
return config["version"].value<QString>();
// 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);

View File

@@ -3,6 +3,7 @@
#ifdef TINYORM_MYSQL_PING
# include <QDebug>
#endif
#include <QVersionNumber>
#include <QtSql/QSqlDriver>
#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<SchemaBuilder> MySqlConnection::getSchemaBuilder()
return std::make_unique<SchemaNs::MySqlSchemaBuilder>(*this);
}
/* Getters */
/* Getters/Setters */
std::optional<QString> 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<QString>();
}
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<QString>()
.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<QVariantHash &>(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<QueryProcessor> 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

View File

@@ -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<QVariantMap> &values,
const QStringList &/*unused*/, const QStringList &update) const
QueryBuilder &query, const QVector<QVariantMap> &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<MySqlConnection &>(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));

View File

@@ -0,0 +1,42 @@
#include "orm/utils/configuration.hpp"
#include <QVariantHash>
#include <QVersionNumber>
#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<QString>()
) {
auto versionValue = version.value<QString>();
// 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

View File

@@ -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 \

View File

@@ -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

View File

@@ -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<MySqlConnection &>(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<MySqlConnection &>(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<MySqlConnection &>(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<MySqlConnection &>(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<MySqlConnection &>(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<MySqlConnection &>(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>({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<MySqlConnection &>(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>({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<MySqlConnection &>(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>({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<MySqlConnection &>(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>({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<MySqlConnection &>(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>({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<MySqlConnection &>(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>({QVariant(QString("pink")), QVariant(0), QVariant(2),
QVariant(QString("purple")), QVariant(4), QVariant(1)}));

View File

@@ -86,6 +86,7 @@ std::shared_ptr<DatabaseManager> setupManager()
{strict_, true},
{isolation_level, QStringLiteral("REPEATABLE READ")},
{engine_, InnoDB},
{Version, {}}, // Autodetect
{options_, QVariantHash()},
}},