mirror of
https://github.com/silverqx/TinyORM.git
synced 2026-02-13 22:09:23 -06:00
drivers added SqlDatabase::record(tableName)
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 🕺
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
#ifndef ORM_DRIVERS_CONCERNS_SELECTSALLCOLUMNSWITHLIMIT0_HPP
|
||||
#define ORM_DRIVERS_CONCERNS_SELECTSALLCOLUMNSWITHLIMIT0_HPP
|
||||
|
||||
#include <orm/macros/systemheader.hpp>
|
||||
TINY_SYSTEM_HEADER
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <orm/macros/commonnamespace.hpp>
|
||||
|
||||
#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<SqlDriver> &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
|
||||
@@ -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;
|
||||
|
||||
@@ -9,10 +9,8 @@ TINY_SYSTEM_HEADER
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <orm/macros/commonnamespace.hpp>
|
||||
|
||||
#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<SqlResult>
|
||||
createResult(const std::weak_ptr<SqlDriver> &driver) const = 0;
|
||||
|
||||
/*! Get a SqlRecord containing the field information for the given table. */
|
||||
virtual SqlRecord
|
||||
record(const QString &table, const std::weak_ptr<SqlDriver> &driver) const = 0;
|
||||
/*! Get a SqlRecord containing the field information for the given table. */
|
||||
virtual SqlRecord
|
||||
recordWithDefaultValues(const QString &table,
|
||||
const std::weak_ptr<SqlDriver> &driver) const = 0;
|
||||
|
||||
protected:
|
||||
/* Setters */
|
||||
/*! Set a flag whether the connection is open. */
|
||||
|
||||
@@ -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<typename T>
|
||||
using NotNull = Orm::Drivers::Utils::NotNull<T>;
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<SqlDriver> &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<const SqlDriver &>(*this);
|
||||
}
|
||||
|
||||
} // namespace Orm::Drivers::Concerns
|
||||
|
||||
TINYORM_END_COMMON_NAMESPACE
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -73,6 +73,14 @@ namespace Orm::Drivers::MySql
|
||||
/*! Factory method to create an empty MySQL result. */
|
||||
std::unique_ptr<SqlResult>
|
||||
createResult(const std::weak_ptr<SqlDriver> &driver) const final;
|
||||
|
||||
/*! Get a SqlRecord containing the field information for the given table. */
|
||||
SqlRecord record(const QString &table,
|
||||
const std::weak_ptr<SqlDriver> &driver) const final;
|
||||
/*! Get a SqlRecord containing the field information for the given table. */
|
||||
SqlRecord
|
||||
recordWithDefaultValues(const QString &table,
|
||||
const std::weak_ptr<SqlDriver> &driver) const final;
|
||||
};
|
||||
|
||||
/* public */
|
||||
|
||||
@@ -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<SqlDriver> &driver) const
|
||||
}
|
||||
}
|
||||
|
||||
SqlRecord
|
||||
MySqlDriver::record(const QString &table, const std::weak_ptr<SqlDriver> &driver) const
|
||||
{
|
||||
return selectAllColumnsWithLimit0(table, driver).record(false);
|
||||
}
|
||||
|
||||
SqlRecord
|
||||
MySqlDriver::recordWithDefaultValues(const QString &table,
|
||||
const std::weak_ptr<SqlDriver> &driver) const
|
||||
{
|
||||
return selectAllColumnsWithLimit0(table, driver).recordAllColumns(true);
|
||||
}
|
||||
|
||||
} // namespace Orm::Drivers::MySql
|
||||
|
||||
TINYORM_END_COMMON_NAMESPACE
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
add_subdirectory(sqldatabase)
|
||||
add_subdirectory(sqldatabasemanager)
|
||||
add_subdirectory(sqlquery_normal)
|
||||
add_subdirectory(sqlquery_prepared)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS = \
|
||||
sqldatabase \
|
||||
sqldatabasemanager \
|
||||
sqlquery_normal \
|
||||
sqlquery_prepared \
|
||||
|
||||
12
tests/auto/functional/drivers/sqldatabase/CMakeLists.txt
Normal file
12
tests/auto/functional/drivers/sqldatabase/CMakeLists.txt
Normal file
@@ -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)
|
||||
@@ -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
|
||||
349
tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp
Normal file
349
tests/auto/functional/drivers/sqldatabase/tst_sqldatabase.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QtTest>
|
||||
|
||||
#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<QString>("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<bool> expectedAutoIncrements {true, false, false, false, false, false};
|
||||
QList<bool> actualAutoIncrements;
|
||||
actualAutoIncrements.reserve(recordCount);
|
||||
|
||||
// NULL in the table definition (not the QVariant value itself)
|
||||
QList<bool> expectedNullColumns {false, true, false, true, false, true};
|
||||
QList<bool> actualNullColumns;
|
||||
actualNullColumns.reserve(recordCount);
|
||||
|
||||
QList<QMetaType> expectedMetaTypes { // clazy:exclude=missing-typeinfo
|
||||
QMetaType::fromType<quint64>(), QMetaType::fromType<quint64>(),
|
||||
QMetaType::fromType<quint64>(), QMetaType::fromType<double>(),
|
||||
QMetaType::fromType<QDateTime>(), QMetaType::fromType<QString>(),
|
||||
};
|
||||
QList<QMetaType> actualMetaTypes; // clazy:exclude=missing-typeinfo
|
||||
actualMetaTypes.reserve(recordCount);
|
||||
|
||||
static const auto BIGINT = u"BIGINT"_s;
|
||||
QList<QString> expectedSqlTypeNames {
|
||||
BIGINT, BIGINT, BIGINT, u"DECIMAL"_s, u"DATETIME"_s, u"VARCHAR"_s,
|
||||
};
|
||||
QList<QString> actualSqlTypeNames;
|
||||
actualSqlTypeNames.reserve(recordCount);
|
||||
|
||||
QList<qint64> expectedLengths {20, 20, 20, 10, 19, 1020};
|
||||
QList<qint64> actualLengths;
|
||||
actualLengths.reserve(recordCount);
|
||||
|
||||
QList<qint64> expectedPrecisions {0, 0, 0, 2, 0, 0};
|
||||
QList<qint64> actualPrecisions;
|
||||
actualPrecisions.reserve(recordCount);
|
||||
|
||||
QList<QVariant> expectedValues {
|
||||
NullVariant::ULongLong(), NullVariant::ULongLong(), NullVariant::ULongLong(),
|
||||
NullVariant::Double() ,NullVariant::QDateTime(), NullVariant::QString(),
|
||||
};
|
||||
QList<QVariant> actualValues;
|
||||
actualValues.reserve(recordCount);
|
||||
|
||||
QList<QVariant> expectedDefaultValues = std::invoke([&connection]() -> QList<QVariant>
|
||||
{
|
||||
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<QVariant> 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<bool> expectedAutoIncrements {true, false, false, false, false, false};
|
||||
QList<bool> actualAutoIncrements;
|
||||
actualAutoIncrements.reserve(recordCount);
|
||||
|
||||
// NULL in the table definition (not the QVariant value itself)
|
||||
QList<bool> expectedNullColumns {false, true, false, true, false, true};
|
||||
QList<bool> actualNullColumns;
|
||||
actualNullColumns.reserve(recordCount);
|
||||
|
||||
QList<QMetaType> expectedMetaTypes { // clazy:exclude=missing-typeinfo
|
||||
QMetaType::fromType<quint64>(), QMetaType::fromType<quint64>(),
|
||||
QMetaType::fromType<quint64>(), QMetaType::fromType<double>(),
|
||||
QMetaType::fromType<QDateTime>(), QMetaType::fromType<QString>(),
|
||||
};
|
||||
QList<QMetaType> actualMetaTypes; // clazy:exclude=missing-typeinfo
|
||||
actualMetaTypes.reserve(recordCount);
|
||||
|
||||
static const auto BIGINT = u"BIGINT"_s;
|
||||
QList<QString> expectedSqlTypeNames {
|
||||
BIGINT, BIGINT, BIGINT, u"DECIMAL"_s, u"DATETIME"_s, u"VARCHAR"_s,
|
||||
};
|
||||
QList<QString> actualSqlTypeNames;
|
||||
actualSqlTypeNames.reserve(recordCount);
|
||||
|
||||
QList<qint64> expectedLengths {20, 20, 20, 10, 19, 1020};
|
||||
QList<qint64> actualLengths;
|
||||
actualLengths.reserve(recordCount);
|
||||
|
||||
QList<qint64> expectedPrecisions {0, 0, 0, 2, 0, 0};
|
||||
QList<qint64> actualPrecisions;
|
||||
actualPrecisions.reserve(recordCount);
|
||||
|
||||
QList<QVariant> expectedValues {
|
||||
NullVariant::ULongLong(), NullVariant::ULongLong(), NullVariant::ULongLong(),
|
||||
NullVariant::Double() ,NullVariant::QDateTime(), NullVariant::QString(),
|
||||
};
|
||||
QList<QVariant> actualValues;
|
||||
actualValues.reserve(recordCount);
|
||||
|
||||
QList<QVariant> expectedDefaultValues = std::invoke([&connection]() -> QList<QVariant>
|
||||
{
|
||||
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<QVariant> 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"
|
||||
@@ -218,11 +218,12 @@ void tst_SchemaBuilder::getAllTables() const
|
||||
const auto tablesActual = getAllTablesFor(connection);
|
||||
|
||||
const QSet<QString> 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);
|
||||
|
||||
12
tests/testdata/create_and_seed_database.php
vendored
12
tests/testdata/create_and_seed_database.php
vendored
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <tom/migration.hpp>
|
||||
|
||||
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
|
||||
@@ -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<DatabaseSeeder>()
|
||||
// Fire it up 🔥🚀✨
|
||||
.run();
|
||||
|
||||
Reference in New Issue
Block a user