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:
silverqx
2024-07-20 19:58:54 +02:00
parent 6472bb6063
commit c4430453e6
21 changed files with 642 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. */

View File

@@ -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. */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
add_subdirectory(sqldatabase)
add_subdirectory(sqldatabasemanager)
add_subdirectory(sqlquery_normal)
add_subdirectory(sqlquery_prepared)

View File

@@ -1,6 +1,7 @@
TEMPLATE = subdirs
SUBDIRS = \
sqldatabase \
sqldatabasemanager \
sqlquery_normal \
sqlquery_prepared \

View 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)

View File

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

View 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"

View File

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

View File

@@ -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();
});
}
/**

View File

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

View File

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