From c4430453e6d6c24fbfa69c148b89ffbae14782df Mon Sep 17 00:00:00 2001 From: silverqx Date: Sat, 20 Jul 2024 19:58:54 +0200 Subject: [PATCH] drivers added SqlDatabase::record(tableName) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It allows to obtain a SqlRecord for the given table. Populating the Default Column Values works the same way as for the SqlQuery/SqlResult couterparts. The recordCached() counterparts in SqlResult were not implemented because of cache invalidation problems (not possible with the current API, it's hard to implement). This API always select-s all columns from the information_schema.columns table, that's why the SqlResult::recordWithDefaultValues(allColumns) has the allColumns parameter, it's used but this API/feature. - added functional tests - added a new empty_with_default_values table - logic that doesn't fit into the MySqlDriver class was extracted to the SelectsAllColumnsWithLimit0 concern 🕺 --- cmake/Modules/TinySources.cmake | 2 + drivers/common/include/include.pri | 1 + .../concerns/selectsallcolumnswithlimit0.hpp | 59 +++ .../include/orm/drivers/sqldatabase.hpp | 5 + .../common/include/orm/drivers/sqldriver.hpp | 17 +- .../common/include/orm/drivers/sqlquery.hpp | 17 + .../concerns/selectsallcolumnswithlimit0.cpp | 54 +++ .../common/src/orm/drivers/sqldatabase.cpp | 26 ++ drivers/common/src/orm/drivers/sqlquery.cpp | 15 + drivers/common/src/src.pri | 1 + .../include/orm/drivers/mysql/mysqldriver.hpp | 8 + .../src/orm/drivers/mysql/mysqldriver.cpp | 15 + tests/auto/functional/drivers/CMakeLists.txt | 1 + tests/auto/functional/drivers/drivers.pro | 1 + .../drivers/sqldatabase/CMakeLists.txt | 12 + .../drivers/sqldatabase/sqldatabase.pro | 7 + .../drivers/sqldatabase/tst_sqldatabase.cpp | 349 ++++++++++++++++++ .../schemabuilder/tst_schemabuilder.cpp | 11 +- tests/testdata/create_and_seed_database.php | 12 + ...create_empty_with_default_values_table.hpp | 36 ++ tests/testdata_tom/main.cpp | 4 +- 21 files changed, 642 insertions(+), 11 deletions(-) create mode 100644 drivers/common/include/orm/drivers/concerns/selectsallcolumnswithlimit0.hpp create mode 100644 drivers/common/src/orm/drivers/concerns/selectsallcolumnswithlimit0.cpp create mode 100644 tests/auto/functional/drivers/sqldatabase/CMakeLists.txt create mode 100644 tests/auto/functional/drivers/sqldatabase/sqldatabase.pro create mode 100644 tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp create mode 100644 tests/testdata_tom/database/migrations/2022_05_11_172000_create_empty_with_default_values_table.hpp diff --git a/cmake/Modules/TinySources.cmake b/cmake/Modules/TinySources.cmake index 18e82dd71..67a12d4d7 100644 --- a/cmake/Modules/TinySources.cmake +++ b/cmake/Modules/TinySources.cmake @@ -31,6 +31,7 @@ function(tinydrivers_sources out_headers_private out_headers out_sources) set(headers) list(APPEND headers + concerns/selectsallcolumnswithlimit0.hpp driverstypes.hpp dummysqlerror.hpp exceptions/driverserror.hpp @@ -66,6 +67,7 @@ function(tinydrivers_sources out_headers_private out_headers out_sources) endif() list(APPEND sources + concerns/selectsallcolumnswithlimit0.cpp dummysqlerror.cpp exceptions/logicerror.cpp exceptions/queryerror.cpp diff --git a/drivers/common/include/include.pri b/drivers/common/include/include.pri index e801b4d18..4106da57b 100644 --- a/drivers/common/include/include.pri +++ b/drivers/common/include/include.pri @@ -1,6 +1,7 @@ INCLUDEPATH *= $$PWD headersList = \ + $$PWD/orm/drivers/concerns/selectsallcolumnswithlimit0.hpp \ $$PWD/orm/drivers/driverstypes.hpp \ $$PWD/orm/drivers/dummysqlerror.hpp \ $$PWD/orm/drivers/exceptions/driverserror.hpp \ diff --git a/drivers/common/include/orm/drivers/concerns/selectsallcolumnswithlimit0.hpp b/drivers/common/include/orm/drivers/concerns/selectsallcolumnswithlimit0.hpp new file mode 100644 index 000000000..3ae6b48be --- /dev/null +++ b/drivers/common/include/orm/drivers/concerns/selectsallcolumnswithlimit0.hpp @@ -0,0 +1,59 @@ +#pragma once +#ifndef ORM_DRIVERS_CONCERNS_SELECTSALLCOLUMNSWITHLIMIT0_HPP +#define ORM_DRIVERS_CONCERNS_SELECTSALLCOLUMNSWITHLIMIT0_HPP + +#include +TINY_SYSTEM_HEADER + +#include + +#include + +#include "orm/drivers/macros/export.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +namespace Orm::Drivers +{ + + class SqlDriver; + class SqlQuery; + +namespace Concerns +{ + + /*! Select all columns in the given table with LIMIT 0 (used by record()). */ + class TINYDRIVERS_EXPORT SelectsAllColumnsWithLimit0 + { + Q_DISABLE_COPY_MOVE(SelectsAllColumnsWithLimit0) + + public: + /*! Pure virtual destructor. */ + inline virtual ~SelectsAllColumnsWithLimit0() = 0; + + protected: + /*! Default constructor. */ + SelectsAllColumnsWithLimit0() = default; + + /* Others */ + /*! Select all columns in the given table with LIMIT 0 (used by record()). */ + SqlQuery + selectAllColumnsWithLimit0(const QString &table, + const std::weak_ptr &driver) const; + + private: + /* Others */ + /*! Dynamic cast *this to the SqlDriver & derived type. */ + const SqlDriver &sqlDriver() const; + }; + + /* public */ + + SelectsAllColumnsWithLimit0::~SelectsAllColumnsWithLimit0() = default; + +} // namespace Concerns +} // namespace Orm::Drivers + +TINYORM_END_COMMON_NAMESPACE + +#endif // ORM_DRIVERS_CONCERNS_SELECTSALLCOLUMNSWITHLIMIT0_HPP diff --git a/drivers/common/include/orm/drivers/sqldatabase.hpp b/drivers/common/include/orm/drivers/sqldatabase.hpp index 2e7a86c4d..5c1d4d8b0 100644 --- a/drivers/common/include/orm/drivers/sqldatabase.hpp +++ b/drivers/common/include/orm/drivers/sqldatabase.hpp @@ -16,6 +16,7 @@ namespace Orm::Drivers class DummySqlError; class SqlDatabasePrivate; + class SqlRecord; /*! Database connection. */ class TINYDRIVERS_EXPORT SqlDatabase : public SqlDatabaseManager // clazy:exclude=rule-of-three @@ -140,6 +141,10 @@ namespace Orm::Drivers /*! Rollback the active database transaction. */ bool rollback(); + /* Others */ + /*! Get a SqlRecord containing the field information for the given table. */ + SqlRecord record(const QString &table, bool withDefaultValues = true) const; + private: /*! Set the connection name. */ void setConnectionName(const QString &connection) noexcept; diff --git a/drivers/common/include/orm/drivers/sqldriver.hpp b/drivers/common/include/orm/drivers/sqldriver.hpp index 2a2373c70..8300e1fcb 100644 --- a/drivers/common/include/orm/drivers/sqldriver.hpp +++ b/drivers/common/include/orm/drivers/sqldriver.hpp @@ -9,10 +9,8 @@ TINY_SYSTEM_HEADER #include -#include - +#include "orm/drivers/concerns/selectsallcolumnswithlimit0.hpp" #include "orm/drivers/driverstypes.hpp" -#include "orm/drivers/macros/export.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -21,10 +19,11 @@ namespace Orm::Drivers class DummySqlError; class SqlDriverPrivate; + class SqlRecord; class SqlResult; /*! Database driver abstract class. */ - class TINYDRIVERS_EXPORT SqlDriver + class TINYDRIVERS_EXPORT SqlDriver : public Concerns::SelectsAllColumnsWithLimit0 { Q_DISABLE_COPY_MOVE(SqlDriver) Q_DECLARE_PRIVATE(SqlDriver) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) @@ -101,7 +100,7 @@ namespace Orm::Drivers }; /*! Pure virtual destructor. */ - virtual ~SqlDriver() = 0; + ~SqlDriver() override = 0; /*! Open the database connection using the given connection values. */ virtual bool @@ -179,6 +178,14 @@ namespace Orm::Drivers virtual std::unique_ptr createResult(const std::weak_ptr &driver) const = 0; + /*! Get a SqlRecord containing the field information for the given table. */ + virtual SqlRecord + record(const QString &table, const std::weak_ptr &driver) const = 0; + /*! Get a SqlRecord containing the field information for the given table. */ + virtual SqlRecord + recordWithDefaultValues(const QString &table, + const std::weak_ptr &driver) const = 0; + protected: /* Setters */ /*! Set a flag whether the connection is open. */ diff --git a/drivers/common/include/orm/drivers/sqlquery.hpp b/drivers/common/include/orm/drivers/sqlquery.hpp index f2eec57ec..b060a8d49 100644 --- a/drivers/common/include/orm/drivers/sqlquery.hpp +++ b/drivers/common/include/orm/drivers/sqlquery.hpp @@ -22,11 +22,23 @@ namespace Orm::Drivers class SqlRecord; class SqlResult; +#ifdef TINYDRIVERS_MYSQL_DRIVER +namespace MySql +{ + class MySqlDriver; +} +#endif + /*! SqlQuery class executes, navigates, and retrieves data from SQL statements. */ class TINYDRIVERS_EXPORT SqlQuery { Q_DISABLE_COPY(SqlQuery) +#ifdef TINYDRIVERS_MYSQL_DRIVER + // To access the recordAllColumns() + friend MySql::MySqlDriver; +#endif + /*! Alias for the NotNull. */ template using NotNull = Orm::Drivers::Utils::NotNull; @@ -179,6 +191,11 @@ namespace Orm::Drivers void throwIfEmptyQueryString(const QString &query); /* Result sets */ +#ifdef TINYDRIVERS_MYSQL_DRIVER + /*! Get a SqlRecord containing the field information for the current query. */ + SqlRecord recordAllColumns(bool withDefaultValues = true) const; +#endif + /*! Normal seek. */ bool seekArbitrary(size_type index, size_type &actualIdx) noexcept; /*! Relative seek. */ diff --git a/drivers/common/src/orm/drivers/concerns/selectsallcolumnswithlimit0.cpp b/drivers/common/src/orm/drivers/concerns/selectsallcolumnswithlimit0.cpp new file mode 100644 index 000000000..caf8aa93f --- /dev/null +++ b/drivers/common/src/orm/drivers/concerns/selectsallcolumnswithlimit0.cpp @@ -0,0 +1,54 @@ +#include "orm/drivers/concerns/selectsallcolumnswithlimit0.hpp" + +#include "orm/drivers/sqldriver.hpp" +#include "orm/drivers/sqlquery.hpp" +#include "orm/drivers/sqlresult.hpp" + +TINYORM_BEGIN_COMMON_NAMESPACE + +using namespace Qt::StringLiterals; // NOLINT(google-build-using-namespace) + +namespace Orm::Drivers::Concerns +{ + +/* By extracting this method to own Concern, the SqlQuery and SqlResult dependency + for SqlDriver was dropped. These classes are required to make a database query + and the SqlDriver class doesn't need them to function properly, in short, + they have nothing to do there. 😮🕺 */ + +/* public */ + +SqlQuery +SelectsAllColumnsWithLimit0::selectAllColumnsWithLimit0( + const QString &table, const std::weak_ptr &driver) const +{ + const auto &sqlDriver = this->sqlDriver(); + + SqlQuery query(sqlDriver.createResult(driver)); + + /* Don't check if a table exists in the currently selected database because + it doesn't make sense, leave the defaults on the database server. + The user can select from any database if the database server allows it. */ + + static const auto queryStringTmpl = u"select * from %1 limit 0"_s; + + /*! Alias for the TableName. */ + constexpr static auto TableName = SqlDriver::IdentifierType::TableName; + + query.exec(queryStringTmpl.arg(sqlDriver.escapeIdentifier(table, TableName))); + + return query; +} + +/* private */ + +/* Others */ + +const SqlDriver &SelectsAllColumnsWithLimit0::sqlDriver() const +{ + return dynamic_cast(*this); +} + +} // namespace Orm::Drivers::Concerns + +TINYORM_END_COMMON_NAMESPACE diff --git a/drivers/common/src/orm/drivers/sqldatabase.cpp b/drivers/common/src/orm/drivers/sqldatabase.cpp index 3aada98d3..be8c98d67 100644 --- a/drivers/common/src/orm/drivers/sqldatabase.cpp +++ b/drivers/common/src/orm/drivers/sqldatabase.cpp @@ -9,6 +9,7 @@ #include "orm/drivers/dummysqlerror.hpp" #include "orm/drivers/sqldatabase_p.hpp" #include "orm/drivers/sqldriver.hpp" +#include "orm/drivers/sqlrecord.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -337,6 +338,31 @@ bool SqlDatabase::rollback() return false; // Don't throw exception here to help avoid #ifdef-s in user's code } +/* Others */ + +/* Don't implement the recardCached() methods here because we would have to cache them + by the table name, that wouldn't be a problem, but problem is that there are no good + code points where these caches can be invalidated (the correct way would be to detect + the DML queries if they are manipulating cached table columns, and that is currently + impossible), so the user will have to manage it himself. */ + +SqlRecord SqlDatabase::record(const QString &table, const bool withDefaultValues) const +{ + /* Will provide information about all fields such as length, precision, + SQL column types, auto-incrementing, field values, ..., and optionally + the Default Column Values. + The difference between this method and the SqlQuery/SqlResult::record() is that + the later is populated during looping over the SqlResult so for the particular + row and the SqlField will also contain the field value. Also, it will only contain + field information for the select-ed columns. + This method will contain field information for ALL columns for the given table, + but of course without the field values. */ + if (withDefaultValues) + return d->driver().recordWithDefaultValues(table, d->sqldriver); + + return d->driver().record(table, d->sqldriver); +} + /* private */ void SqlDatabase::setConnectionName(const QString &connection) noexcept diff --git a/drivers/common/src/orm/drivers/sqlquery.cpp b/drivers/common/src/orm/drivers/sqlquery.cpp index 0791a8e70..c82aac9b3 100644 --- a/drivers/common/src/orm/drivers/sqlquery.cpp +++ b/drivers/common/src/orm/drivers/sqlquery.cpp @@ -509,6 +509,21 @@ void SqlQuery::throwIfEmptyQueryString(const QString &query) /* Result sets */ +#ifdef TINYDRIVERS_MYSQL_DRIVER +SqlRecord SqlQuery::recordAllColumns(const bool withDefaultValues) const +{ + throwIfNoResultSet(); + + /* Will provide information about all fields such as length, precision, + SQL column types, auto-incrementing, field values, ..., and optionally + the Default Column Values. */ + if (withDefaultValues) + return m_sqlResult->recordWithDefaultValues(true); + + return m_sqlResult->record(); +} +#endif + bool SqlQuery::seekArbitrary(const size_type index, size_type &actualIdx) noexcept { // Nothing to do diff --git a/drivers/common/src/src.pri b/drivers/common/src/src.pri index d308754fe..63906d5f9 100644 --- a/drivers/common/src/src.pri +++ b/drivers/common/src/src.pri @@ -7,6 +7,7 @@ build_loadable_drivers: \ sourcesList += $$PWD/orm/drivers/utils/fs_p.cpp sourcesList += \ + $$PWD/orm/drivers/concerns/selectsallcolumnswithlimit0.cpp \ $$PWD/orm/drivers/dummysqlerror.cpp \ $$PWD/orm/drivers/exceptions/logicerror.cpp \ $$PWD/orm/drivers/exceptions/queryerror.cpp \ diff --git a/drivers/mysql/include/orm/drivers/mysql/mysqldriver.hpp b/drivers/mysql/include/orm/drivers/mysql/mysqldriver.hpp index 897e66223..d7acabd95 100644 --- a/drivers/mysql/include/orm/drivers/mysql/mysqldriver.hpp +++ b/drivers/mysql/include/orm/drivers/mysql/mysqldriver.hpp @@ -73,6 +73,14 @@ namespace Orm::Drivers::MySql /*! Factory method to create an empty MySQL result. */ std::unique_ptr createResult(const std::weak_ptr &driver) const final; + + /*! Get a SqlRecord containing the field information for the given table. */ + SqlRecord record(const QString &table, + const std::weak_ptr &driver) const final; + /*! Get a SqlRecord containing the field information for the given table. */ + SqlRecord + recordWithDefaultValues(const QString &table, + const std::weak_ptr &driver) const final; }; /* public */ diff --git a/drivers/mysql/src/orm/drivers/mysql/mysqldriver.cpp b/drivers/mysql/src/orm/drivers/mysql/mysqldriver.cpp index 62a165b5e..62f8153c2 100644 --- a/drivers/mysql/src/orm/drivers/mysql/mysqldriver.cpp +++ b/drivers/mysql/src/orm/drivers/mysql/mysqldriver.cpp @@ -8,6 +8,8 @@ #include "orm/drivers/mysql/mysqldriver_p.hpp" #include "orm/drivers/mysql/mysqlresult.hpp" #include "orm/drivers/mysql/mysqlutils_p.hpp" +#include "orm/drivers/sqlquery.hpp" +#include "orm/drivers/sqlrecord.hpp" #include "orm/drivers/utils/type_p.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -246,6 +248,19 @@ MySqlDriver::createResult(const std::weak_ptr &driver) const } } +SqlRecord +MySqlDriver::record(const QString &table, const std::weak_ptr &driver) const +{ + return selectAllColumnsWithLimit0(table, driver).record(false); +} + +SqlRecord +MySqlDriver::recordWithDefaultValues(const QString &table, + const std::weak_ptr &driver) const +{ + return selectAllColumnsWithLimit0(table, driver).recordAllColumns(true); +} + } // namespace Orm::Drivers::MySql TINYORM_END_COMMON_NAMESPACE diff --git a/tests/auto/functional/drivers/CMakeLists.txt b/tests/auto/functional/drivers/CMakeLists.txt index 59e7ac1b9..2412851bf 100644 --- a/tests/auto/functional/drivers/CMakeLists.txt +++ b/tests/auto/functional/drivers/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(sqldatabase) add_subdirectory(sqldatabasemanager) add_subdirectory(sqlquery_normal) add_subdirectory(sqlquery_prepared) diff --git a/tests/auto/functional/drivers/drivers.pro b/tests/auto/functional/drivers/drivers.pro index b6def01ef..28d732b1a 100644 --- a/tests/auto/functional/drivers/drivers.pro +++ b/tests/auto/functional/drivers/drivers.pro @@ -1,6 +1,7 @@ TEMPLATE = subdirs SUBDIRS = \ + sqldatabase \ sqldatabasemanager \ sqlquery_normal \ sqlquery_prepared \ diff --git a/tests/auto/functional/drivers/sqldatabase/CMakeLists.txt b/tests/auto/functional/drivers/sqldatabase/CMakeLists.txt new file mode 100644 index 000000000..29e127601 --- /dev/null +++ b/tests/auto/functional/drivers/sqldatabase/CMakeLists.txt @@ -0,0 +1,12 @@ +project(sqldatabase + LANGUAGES CXX +) + +add_executable(sqldatabase + tst_sqldatabase.cpp +) + +add_test(NAME sqldatabase COMMAND sqldatabase) + +include(TinyTestCommon) +tiny_configure_test(sqldatabase) diff --git a/tests/auto/functional/drivers/sqldatabase/sqldatabase.pro b/tests/auto/functional/drivers/sqldatabase/sqldatabase.pro new file mode 100644 index 000000000..d6e55942f --- /dev/null +++ b/tests/auto/functional/drivers/sqldatabase/sqldatabase.pro @@ -0,0 +1,7 @@ +# Add the TinyDrivers include path as a non-system include path +TINY_DRIVERS_INCLUDE_NONSYSTEM = true + +include($$TINYORM_SOURCE_TREE/tests/qmake/common.pri) +include($$TINYORM_SOURCE_TREE/tests/qmake/TinyUtils.pri) + +SOURCES += tst_sqldatabase.cpp diff --git a/tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp b/tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp new file mode 100644 index 000000000..b24bc10ec --- /dev/null +++ b/tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp @@ -0,0 +1,349 @@ +#include +#include + +#include "orm/drivers/sqlrecord.hpp" + +#include "orm/constants.hpp" +#include "orm/utils/nullvariant.hpp" +#include "orm/utils/type.hpp" + +#include "databases.hpp" + +using namespace Qt::StringLiterals; // NOLINT(google-build-using-namespace) + +using Orm::Constants::ID; +using Orm::Constants::NOTE; +using Orm::Constants::SIZE_; + +using Orm::Drivers::SqlDatabase; +using Orm::Drivers::SqlRecord; + +using Orm::Utils::NullVariant; + +using TypeUtils = Orm::Utils::Type; + +using TestUtils::Databases; + +class tst_SqlDatabase : public QObject // clazy:exclude=ctor-missing-parent-argument +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() const; + + void table_record_WithDefaultValues() const; + void table_record_WithoutDefaultValues() const; +}; + +/* private slots */ + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +void tst_SqlDatabase::initTestCase() const +{ + const auto connections = Databases::createDriversConnections(); + + if (connections.isEmpty()) + QSKIP(TestUtils::AutoTestSkippedAny.arg(TypeUtils::classPureBasename(*this)) + .toUtf8().constData(), ); + + QTest::addColumn("connection"); + + // Run all tests for all supported database connections + for (const auto &connection : connections) + QTest::newRow(connection.toUtf8().constData()) << connection; +} + +// This is an overkill, but it tests everything, I think I have a lot of free time 😁😅 +void tst_SqlDatabase::table_record_WithDefaultValues() const +{ + QFETCH_GLOBAL(QString, connection); // NOLINT(modernize-type-traits) + + static const auto EmptyWithDefaultValues = u"empty_with_default_values"_s; + + const auto db = SqlDatabase::database(connection); + QVERIFY(db.isValid()); + QVERIFY(db.isOpen()); + QVERIFY(!db.isOpenError()); + // Don't uncomment to test the default argument + const auto record = db.record(EmptyWithDefaultValues/*, true*/); + + // Verify the record + QVERIFY(!record.isEmpty()); + const auto recordCount = record.count(); + QCOMPARE(recordCount, 6); + + // Populate values to compare + // Column definitions related only + QList expectedAutoIncrements {true, false, false, false, false, false}; + QList actualAutoIncrements; + actualAutoIncrements.reserve(recordCount); + + // NULL in the table definition (not the QVariant value itself) + QList expectedNullColumns {false, true, false, true, false, true}; + QList actualNullColumns; + actualNullColumns.reserve(recordCount); + + QList expectedMetaTypes { // clazy:exclude=missing-typeinfo + QMetaType::fromType(), QMetaType::fromType(), + QMetaType::fromType(), QMetaType::fromType(), + QMetaType::fromType(), QMetaType::fromType(), + }; + QList actualMetaTypes; // clazy:exclude=missing-typeinfo + actualMetaTypes.reserve(recordCount); + + static const auto BIGINT = u"BIGINT"_s; + QList expectedSqlTypeNames { + BIGINT, BIGINT, BIGINT, u"DECIMAL"_s, u"DATETIME"_s, u"VARCHAR"_s, + }; + QList actualSqlTypeNames; + actualSqlTypeNames.reserve(recordCount); + + QList expectedLengths {20, 20, 20, 10, 19, 1020}; + QList actualLengths; + actualLengths.reserve(recordCount); + + QList expectedPrecisions {0, 0, 0, 2, 0, 0}; + QList actualPrecisions; + actualPrecisions.reserve(recordCount); + + QList expectedValues { + NullVariant::ULongLong(), NullVariant::ULongLong(), NullVariant::ULongLong(), + NullVariant::Double() ,NullVariant::QDateTime(), NullVariant::QString(), + }; + QList actualValues; + actualValues.reserve(recordCount); + + QList expectedDefaultValues = std::invoke([&connection]() -> QList + { + static const auto NULL_ = u"NULL"_s; + static const auto Zero = u"0"_s; + + /* MySQL and MariaDB have different values in the COLUMN_DEFAULT column: + MySQL: NULL, "CURRENT_TIMESTAMP" + MariaDB: "NULL", "current_timestamp()" + MariaDB has string "NULL" in COLUMN_DEFAULT column if IS_NULLABLE="YES", + MySQL uses normal SQL NULL and you must check the IS_NULLABLE column + to find out if a column is nullable. + Also, MySQL returns QByteArray because it has set the BINARY attribute + on the COLUMN_DEFAULT column because it uses the utf8mb3_bin collation, + the flags=144 (BLOB_FLAG, BINARY_FLAG). I spent a lot of time on this to + find out. 🤔 + MariaDB uses the utf8mb3_general_ci so it returns the QString, + flags=4112 (BLOB_FLAG, NO_DEFAULT_VALUE_FLAG). */ + if (connection == Databases::MYSQL_DRIVERS) + return {NullVariant::QByteArray(), NullVariant::QByteArray(), + Zero.toUtf8(), u"100.12"_s.toUtf8(), u"CURRENT_TIMESTAMP"_s.toUtf8(), + NullVariant::QByteArray()}; + + if (connection == Databases::MARIADB_DRIVERS) + return {NullVariant::QString(), NULL_, Zero, u"100.12"_s, + u"current_timestamp()"_s, NULL_}; + + Q_UNREACHABLE(); + }); + QList actualDefaultValues; + actualDefaultValues.reserve(recordCount); + + for (SqlRecord::size_type i = 0; i < recordCount; ++i) { + const auto field = record.field(i); + QVERIFY(field.isValid()); + QVERIFY(field.isNull()); + QCOMPARE(field.tableName(), EmptyWithDefaultValues); + + actualAutoIncrements << field.isAutoIncrement(); + actualNullColumns << field.isNullColumn(); + actualMetaTypes << field.metaType(); + actualSqlTypeNames << field.sqlTypeName(); + actualLengths << field.length(); + actualPrecisions << field.precision(); + actualValues << field.value(); + actualDefaultValues << field.defaultValue(); + } + + // Verify all at once + QCOMPARE(record.fieldNames(), + QStringList({ID, "user_id", SIZE_, "decimal", "added_on", NOTE})); + QCOMPARE(actualAutoIncrements, expectedAutoIncrements); + QCOMPARE(actualNullColumns, expectedNullColumns); + QCOMPARE(actualMetaTypes, expectedMetaTypes); + QCOMPARE(actualSqlTypeNames, expectedSqlTypeNames); + QCOMPARE(actualLengths, expectedLengths); + QCOMPARE(actualPrecisions, expectedPrecisions); + QCOMPARE(actualValues, expectedValues); + QCOMPARE(actualDefaultValues, expectedDefaultValues); +} + +void tst_SqlDatabase::table_record_WithoutDefaultValues() const +{ + QFETCH_GLOBAL(QString, connection); // NOLINT(modernize-type-traits) + + static const auto EmptyWithDefaultValues = u"empty_with_default_values"_s; + + const auto db = SqlDatabase::database(connection); + QVERIFY(db.isValid()); + QVERIFY(db.isOpen()); + QVERIFY(!db.isOpenError()); + + const auto record = db.record(EmptyWithDefaultValues, false); + + // Verify the record + QVERIFY(!record.isEmpty()); + const auto recordCount = record.count(); + QCOMPARE(recordCount, 6); + + // Populate values to compare + // Column definitions related only + QList expectedAutoIncrements {true, false, false, false, false, false}; + QList actualAutoIncrements; + actualAutoIncrements.reserve(recordCount); + + // NULL in the table definition (not the QVariant value itself) + QList expectedNullColumns {false, true, false, true, false, true}; + QList actualNullColumns; + actualNullColumns.reserve(recordCount); + + QList expectedMetaTypes { // clazy:exclude=missing-typeinfo + QMetaType::fromType(), QMetaType::fromType(), + QMetaType::fromType(), QMetaType::fromType(), + QMetaType::fromType(), QMetaType::fromType(), + }; + QList actualMetaTypes; // clazy:exclude=missing-typeinfo + actualMetaTypes.reserve(recordCount); + + static const auto BIGINT = u"BIGINT"_s; + QList expectedSqlTypeNames { + BIGINT, BIGINT, BIGINT, u"DECIMAL"_s, u"DATETIME"_s, u"VARCHAR"_s, + }; + QList actualSqlTypeNames; + actualSqlTypeNames.reserve(recordCount); + + QList expectedLengths {20, 20, 20, 10, 19, 1020}; + QList actualLengths; + actualLengths.reserve(recordCount); + + QList expectedPrecisions {0, 0, 0, 2, 0, 0}; + QList actualPrecisions; + actualPrecisions.reserve(recordCount); + + QList expectedValues { + NullVariant::ULongLong(), NullVariant::ULongLong(), NullVariant::ULongLong(), + NullVariant::Double() ,NullVariant::QDateTime(), NullVariant::QString(), + }; + QList actualValues; + actualValues.reserve(recordCount); + + QList expectedDefaultValues = std::invoke([&connection]() -> QList + { + static const auto NULL_ = u"NULL"_s; + static const auto Zero = u"0"_s; + + /* MySQL and MariaDB have different values in the COLUMN_DEFAULT column: + MySQL: NULL, "CURRENT_TIMESTAMP" + MariaDB: "NULL", "current_timestamp()" + MariaDB has string "NULL" in COLUMN_DEFAULT column if IS_NULLABLE="YES", + MySQL uses normal SQL NULL and you must check the IS_NULLABLE column + to find out if a column is nullable. + Also, MySQL returns QByteArray because it has set the BINARY attribute + on the COLUMN_DEFAULT column because it uses the utf8mb3_bin collation, + the flags=144 (BLOB_FLAG, BINARY_FLAG). I spent a lot of time on this to + find out. 🤔 + MariaDB uses the utf8mb3_general_ci so it returns the QString, + flags=4112 (BLOB_FLAG, NO_DEFAULT_VALUE_FLAG). */ + if (connection == Databases::MYSQL_DRIVERS) + return {NullVariant::QByteArray(), NullVariant::QByteArray(), + Zero.toUtf8(), u"100.12"_s.toUtf8(), u"CURRENT_TIMESTAMP"_s.toUtf8(), + NullVariant::QByteArray()}; + + if (connection == Databases::MARIADB_DRIVERS) + return {NullVariant::QString(), NULL_, Zero, u"100.12"_s, + u"current_timestamp()"_s, NULL_}; + + Q_UNREACHABLE(); + }); + QList actualDefaultValues; + actualDefaultValues.reserve(recordCount); + + for (SqlRecord::size_type i = 0; i < recordCount; ++i) { + const auto field = record.field(i); + QVERIFY(field.isValid()); + QVERIFY(field.isNull()); + QCOMPARE(field.tableName(), EmptyWithDefaultValues); + + actualAutoIncrements << field.isAutoIncrement(); + actualNullColumns << field.isNullColumn(); + actualMetaTypes << field.metaType(); + actualSqlTypeNames << field.sqlTypeName(); + actualLengths << field.length(); + actualPrecisions << field.precision(); + actualValues << field.value(); + QVERIFY(!field.defaultValue().isValid()); + } + + // Verify all at once + QCOMPARE(record.fieldNames(), + QStringList({ID, "user_id", SIZE_, "decimal", "added_on", NOTE})); + QCOMPARE(actualAutoIncrements, expectedAutoIncrements); + QCOMPARE(actualNullColumns, expectedNullColumns); + QCOMPARE(actualMetaTypes, expectedMetaTypes); + QCOMPARE(actualSqlTypeNames, expectedSqlTypeNames); + QCOMPARE(actualLengths, expectedLengths); + QCOMPARE(actualPrecisions, expectedPrecisions); + QCOMPARE(actualValues, expectedValues); + + // Clear before the next loop + actualAutoIncrements.clear(); + actualNullColumns.clear(); + actualMetaTypes.clear(); + actualSqlTypeNames.clear(); + actualLengths.clear(); + actualPrecisions.clear(); + actualValues.clear(); + actualDefaultValues.clear(); + + /* Re-create the SqlRecord with Default Column Values, it of course must be done + after all the previous tests as the last thing to test it correctly. */ + const auto recordNew = db.record(EmptyWithDefaultValues, true); + // Non-cached SqlRecord is returned by value + QVERIFY(std::addressof(recordNew) != std::addressof(record)); + + // Verify the record + QVERIFY(!recordNew.isEmpty()); + const auto recordCountNew = recordNew.count(); + QCOMPARE(recordCountNew, recordCount); + + /* Verify re-populated Default Column Values, OK I reinvoke the same test logic + again as everything should stay the same, to correctly test it. */ + + for (SqlRecord::size_type i = 0; i < recordCountNew; ++i) { + const auto field = recordNew.field(i); + QVERIFY(field.isValid()); + QVERIFY(field.isNull()); + QCOMPARE(field.tableName(), EmptyWithDefaultValues); + + actualAutoIncrements << field.isAutoIncrement(); + actualNullColumns << field.isNullColumn(); + actualMetaTypes << field.metaType(); + actualSqlTypeNames << field.sqlTypeName(); + actualLengths << field.length(); + actualPrecisions << field.precision(); + actualValues << field.value(); + actualDefaultValues << field.defaultValue(); + } + + // Verify all at once + QCOMPARE(recordNew.fieldNames(), + QStringList({ID, "user_id", SIZE_, "decimal", "added_on", NOTE})); + QCOMPARE(actualAutoIncrements, expectedAutoIncrements); + QCOMPARE(actualNullColumns, expectedNullColumns); + QCOMPARE(actualMetaTypes, expectedMetaTypes); + QCOMPARE(actualSqlTypeNames, expectedSqlTypeNames); + QCOMPARE(actualLengths, expectedLengths); + QCOMPARE(actualPrecisions, expectedPrecisions); + QCOMPARE(actualValues, expectedValues); + QCOMPARE(actualDefaultValues, expectedDefaultValues); +} +// NOLINTEND(readability-convert-member-functions-to-static) + +QTEST_MAIN(tst_SqlDatabase) + +#include "tst_sqldatabase.moc" diff --git a/tests/auto/functional/orm/schema/schemabuilder/tst_schemabuilder.cpp b/tests/auto/functional/orm/schema/schemabuilder/tst_schemabuilder.cpp index b9734dff5..5bf1a8736 100644 --- a/tests/auto/functional/orm/schema/schemabuilder/tst_schemabuilder.cpp +++ b/tests/auto/functional/orm/schema/schemabuilder/tst_schemabuilder.cpp @@ -218,11 +218,12 @@ void tst_SchemaBuilder::getAllTables() const const auto tablesActual = getAllTablesFor(connection); const QSet tablesExpected { - "albums", "album_images", "datetimes", "file_property_properties", - "migrations", "roles", "role_tag", "role_user", "settings", "state_torrent", - "tag_properties", "tag_torrent", "torrents", "torrent_peers", - "torrent_previewable_files", "torrent_previewable_file_properties", - "torrent_states", "torrent_tags", "types", "users", "user_phones", + "albums", "album_images", "datetimes", "empty_with_default_values", + "file_property_properties", "migrations", "roles", "role_tag", "role_user", + "settings", "state_torrent", "tag_properties", "tag_torrent", "torrents", + "torrent_peers", "torrent_previewable_files", + "torrent_previewable_file_properties", "torrent_states", "torrent_tags", "types", + "users", "user_phones", }; QCOMPARE(tablesActual, tablesExpected); diff --git a/tests/testdata/create_and_seed_database.php b/tests/testdata/create_and_seed_database.php index 3e7d8bbdb..89868f82d 100644 --- a/tests/testdata/create_and_seed_database.php +++ b/tests/testdata/create_and_seed_database.php @@ -376,6 +376,18 @@ function createTables(string $connection): void $table->foreign('role_id')->references('id')->on('roles') ->cascadeOnUpdate()->cascadeOnDelete(); }); + + $schema->create('empty_with_default_values', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id')->nullable(); + $table->unsignedBigInteger('size')->default('0'); + $table->decimal('decimal')->default('100.12')->nullable(); + $table->dateTime('added_on')->useCurrent(); + $table->string('note')->nullable(); + + $table->foreign('user_id')->references('id')->on('users') + ->cascadeOnUpdate()->cascadeOnDelete(); + }); } /** diff --git a/tests/testdata_tom/database/migrations/2022_05_11_172000_create_empty_with_default_values_table.hpp b/tests/testdata_tom/database/migrations/2022_05_11_172000_create_empty_with_default_values_table.hpp new file mode 100644 index 000000000..89f3e64ce --- /dev/null +++ b/tests/testdata_tom/database/migrations/2022_05_11_172000_create_empty_with_default_values_table.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace Migrations +{ + + struct CreateEmptyWithDefaultValuesTable : Migration + { + T_MIGRATION + + /*! Run the migrations. */ + void up() const override + { + Schema::create("empty_with_default_values", [](Blueprint &table) + { + table.id(); + + table.foreignId("user_id").nullable() + .constrained().cascadeOnDelete().cascadeOnUpdate(); + + table.unsignedBigInteger(SIZE_).defaultValue("0"); + table.decimal("decimal").defaultValue("100.12").nullable(); + table.datetime("added_on").useCurrent(); + table.string(NOTE).nullable(); + }); + } + + /*! Reverse the migrations. */ + void down() const override + { + Schema::dropIfExists("empty_with_default_values"); + } + }; + +} // namespace Migrations diff --git a/tests/testdata_tom/main.cpp b/tests/testdata_tom/main.cpp index 6be7b100b..d70824e50 100644 --- a/tests/testdata_tom/main.cpp +++ b/tests/testdata_tom/main.cpp @@ -23,6 +23,7 @@ #include "migrations/2022_05_11_171700_create_torrent_states_table.hpp" #include "migrations/2022_05_11_171800_create_state_torrent_table.hpp" #include "migrations/2022_05_11_171900_create_role_tag_table.hpp" +#include "migrations/2022_05_11_172000_create_empty_with_default_values_table.hpp" #include "seeders/databaseseeder.hpp" @@ -68,7 +69,8 @@ int main(int argc, char *argv[]) CreateAlbumImagesTable, CreateTorrentStatesTable, CreateStateTorrentTable, - CreateRoleTagTable>() + CreateRoleTagTable, + CreateEmptyWithDefaultValuesTable>() .seeders() // Fire it up 🔥🚀✨ .run();