Add a helper class which makes sure that the used table does not exist before usage (e.g. due to leftovers from previous tests) and is properly cleaned up on exit. This also allows to remove all usages of safeDropTable(). Change-Id: Iefeffbd10e2f2f67985183ea822d7b6dd2b80be7 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
522 lines
18 KiB
C++
522 lines
18 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
/* possible connection parameters */
|
|
|
|
#ifndef TST_DATABASES_H
|
|
#define TST_DATABASES_H
|
|
|
|
#include <QSqlDatabase>
|
|
#include <QSqlDriver>
|
|
#include <QSqlError>
|
|
#include <QSqlQuery>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionMatch>
|
|
#include <QDir>
|
|
#include <QScopedPointer>
|
|
#include <QVariant>
|
|
#include <QDebug>
|
|
#include <QSqlTableModel>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonDocument>
|
|
#include <QtSql/private/qsqldriver_p.h>
|
|
#include <QTest>
|
|
|
|
#define CHECK_DATABASE( db ) \
|
|
if ( !db.isValid() ) { qFatal( "db is Invalid" ); }
|
|
|
|
#define QVERIFY_SQL(q, stmt) QVERIFY2((q).stmt, tst_Databases::printError((q).lastError(), db))
|
|
#define QFAIL_SQL(q, stmt) QVERIFY2(!(q).stmt, tst_Databases::printError((q).lastError(), db))
|
|
|
|
#define DBMS_SPECIFIC(db, driver) \
|
|
if (!db.driverName().startsWith(driver)) { QSKIP(driver " specific test"); }
|
|
|
|
// ### use QSystem::hostName if it is integrated in qtest/main
|
|
static QString qGetHostName()
|
|
{
|
|
static QString hostname;
|
|
|
|
if (hostname.isEmpty()) {
|
|
hostname = QSysInfo::machineHostName();
|
|
hostname.replace(QLatin1Char( '.' ), QLatin1Char( '_' ));
|
|
hostname.replace(QLatin1Char( '-' ), QLatin1Char( '_' ));
|
|
}
|
|
|
|
return hostname;
|
|
}
|
|
|
|
inline QString fixupTableName(const QString &tableName, QSqlDatabase db)
|
|
{
|
|
QString tbName = tableName;
|
|
// On Oracle we are limited to 30 character tablenames
|
|
QSqlDriverPrivate *d = static_cast<QSqlDriverPrivate *>(QObjectPrivate::get(db.driver()));
|
|
if (d && d->dbmsType == QSqlDriver::Oracle)
|
|
tbName.truncate(30);
|
|
// On Interbase we are limited to 31 character tablenames
|
|
if (d && d->dbmsType == QSqlDriver::Interbase)
|
|
tbName.truncate(31);
|
|
return tbName;
|
|
}
|
|
|
|
// to prevent nameclashes on our database server, each machine
|
|
// will use its own set of table names. Call this function to get
|
|
// "tablename_hostname"
|
|
inline static QString qTableName(const QString &prefix, const char *sourceFileName,
|
|
QSqlDatabase db, bool escape = true)
|
|
{
|
|
const auto tableStr = fixupTableName(QString(QLatin1String("dbtst") + db.driverName() + "_" +
|
|
prefix + QString::number(qHash(QLatin1String(sourceFileName) +
|
|
"_" + qGetHostName().replace("-", "_")), 16)), db);
|
|
return escape ? db.driver()->escapeIdentifier(tableStr, QSqlDriver::TableName) : tableStr;
|
|
}
|
|
|
|
class tst_Databases
|
|
{
|
|
public:
|
|
~tst_Databases()
|
|
{
|
|
close();
|
|
}
|
|
|
|
// returns a testtable consisting of the names of all database connections if
|
|
// driverPrefix is empty, otherwise only those that start with driverPrefix.
|
|
int fillTestTable( const QString& driverPrefix = QString() ) const
|
|
{
|
|
QTest::addColumn<QString>( "dbName" );
|
|
int count = 0;
|
|
|
|
for ( int i = 0; i < dbNames.size(); ++i ) {
|
|
QSqlDatabase db = QSqlDatabase::database( dbNames.at( i ) );
|
|
|
|
if ( !db.isValid() )
|
|
continue;
|
|
|
|
if ( driverPrefix.isEmpty() || db.driverName().startsWith( driverPrefix ) ) {
|
|
QTest::newRow( dbNames.at( i ).toLatin1() ) << dbNames.at( i );
|
|
++count;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
int fillTestTableWithStrategies( const QString& driverPrefix = QString() ) const
|
|
{
|
|
QTest::addColumn<QString>( "dbName" );
|
|
QTest::addColumn<int>("submitpolicy_i");
|
|
int count = 0;
|
|
|
|
for ( int i = 0; i < dbNames.size(); ++i ) {
|
|
QSqlDatabase db = QSqlDatabase::database( dbNames.at( i ) );
|
|
|
|
if ( !db.isValid() )
|
|
continue;
|
|
|
|
if ( driverPrefix.isEmpty() || db.driverName().startsWith( driverPrefix ) ) {
|
|
QTest::newRow( QString("%1 [field]").arg(dbNames.at( i )).toLatin1() ) << dbNames.at( i ) << (int)QSqlTableModel::OnFieldChange;
|
|
QTest::newRow( QString("%1 [row]").arg(dbNames.at( i )).toLatin1() ) << dbNames.at( i ) << (int)QSqlTableModel::OnRowChange;
|
|
QTest::newRow( QString("%1 [manual]").arg(dbNames.at( i )).toLatin1() ) << dbNames.at( i ) << (int)QSqlTableModel::OnManualSubmit;
|
|
++count;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void addDb( const QString& driver, const QString& dbName,
|
|
const QString& user = QString(), const QString& passwd = QString(),
|
|
const QString& host = QString(), int port = -1, const QString params = QString() )
|
|
{
|
|
QSqlDatabase db;
|
|
|
|
if ( !QSqlDatabase::drivers().contains( driver ) ) {
|
|
qWarning() << "Driver" << driver << "is not installed";
|
|
return;
|
|
}
|
|
|
|
// construct a stupid unique name
|
|
QString cName = QString::number( counter++ ) + QLatin1Char('_') + driver + QLatin1Char('@');
|
|
|
|
cName += host.isEmpty() ? dbName : host;
|
|
|
|
if ( port > 0 )
|
|
cName += QLatin1Char(':') + QString::number( port );
|
|
|
|
if (driver == "QSQLITE") {
|
|
// Since the database for sqlite is generated at runtime it's always
|
|
// available, but we use QTempDir so it's always in a different
|
|
// location. Thus, let's ignore the path completely.
|
|
cName = "SQLite";
|
|
qInfo("SQLite will use the database located at %ls", qUtf16Printable(dbName));
|
|
}
|
|
|
|
db = QSqlDatabase::addDatabase( driver, cName );
|
|
|
|
if ( !db.isValid() ) {
|
|
qWarning( "Could not create database object" );
|
|
return;
|
|
}
|
|
|
|
db.setDatabaseName( dbName );
|
|
|
|
db.setUserName( user );
|
|
db.setPassword( passwd );
|
|
db.setHostName( host );
|
|
db.setPort( port );
|
|
db.setConnectOptions( params );
|
|
dbNames.append( cName );
|
|
}
|
|
|
|
bool addDbs()
|
|
{
|
|
// Test databases can be defined in a file using the following format:
|
|
//
|
|
// {
|
|
// "entries": [
|
|
// {
|
|
// "driver": "QPSQL",
|
|
// "name": "testdb",
|
|
// "username": "postgres",
|
|
// "password": "password",
|
|
// "hostname": "localhost",
|
|
// "port": 5432,
|
|
// "parameters": "extraoptions"
|
|
// },
|
|
// {
|
|
// ....
|
|
// }
|
|
// ]
|
|
// }
|
|
|
|
bool added = false;
|
|
const QString databasesFile(qgetenv("QT_TEST_DATABASES_FILE"));
|
|
QFile f(databasesFile.isEmpty() ? "testdbs.json" : databasesFile);
|
|
if (f.exists() && f.open(QIODevice::ReadOnly)) {
|
|
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
|
f.close();
|
|
const QJsonValue entriesV = doc.object().value(QLatin1String("entries"));
|
|
if (!entriesV.isArray()) {
|
|
qWarning() << "No entries in " + f.fileName();
|
|
} else {
|
|
const QJsonArray entriesA = entriesV.toArray();
|
|
QJsonArray::const_iterator it = entriesA.constBegin();
|
|
while (it != entriesA.constEnd()) {
|
|
if ((*it).isObject()) {
|
|
const QJsonObject object = (*it).toObject();
|
|
addDb(object.value(QStringLiteral("driver")).toString(),
|
|
object.value(QStringLiteral("name")).toString(),
|
|
object.value(QStringLiteral("username")).toString(),
|
|
object.value(QStringLiteral("password")).toString(),
|
|
object.value(QStringLiteral("hostname")).toString(),
|
|
object.value(QStringLiteral("port")).toInt(),
|
|
object.value(QStringLiteral("parameters")).toString());
|
|
added = true;
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
QTemporaryDir *sqLiteDir = dbDir();
|
|
if (sqLiteDir) {
|
|
addDb(QStringLiteral("QSQLITE"), QDir::toNativeSeparators(sqLiteDir->path() + QStringLiteral("/sqlite.db")));
|
|
added = true;
|
|
}
|
|
return added;
|
|
}
|
|
|
|
// 'false' return indicates a system error, for example failure to create a temporary directory.
|
|
bool open()
|
|
{
|
|
if (!addDbs())
|
|
return false;
|
|
|
|
QStringList::Iterator it = dbNames.begin();
|
|
|
|
while ( it != dbNames.end() ) {
|
|
QSqlDatabase db = QSqlDatabase::database(( *it ), false );
|
|
qDebug() << "Opening:" << (*it);
|
|
|
|
if ( db.isValid() && !db.isOpen() ) {
|
|
if ( !db.open() ) {
|
|
qWarning( "tst_Databases: Unable to open %s on %s:\n%s", qPrintable( db.driverName() ), qPrintable( *it ), qPrintable( db.lastError().databaseText() ) );
|
|
// well... opening failed, so we just ignore the server, maybe it is not running
|
|
it = dbNames.erase( it );
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void close()
|
|
{
|
|
for ( QStringList::Iterator it = dbNames.begin(); it != dbNames.end(); ++it ) {
|
|
{
|
|
QSqlDatabase db = QSqlDatabase::database(( *it ), false );
|
|
|
|
if ( db.isValid() && db.isOpen() )
|
|
db.close();
|
|
}
|
|
|
|
QSqlDatabase::removeDatabase(( *it ) );
|
|
}
|
|
|
|
dbNames.clear();
|
|
}
|
|
|
|
// for debugging only: outputs the connection as string
|
|
static QString dbToString( const QSqlDatabase db )
|
|
{
|
|
QString res = db.driverName() + QLatin1Char('@');
|
|
|
|
if ( db.driverName().startsWith( "QODBC" ) || db.driverName().startsWith( "QOCI" ) ) {
|
|
res += db.databaseName();
|
|
} else {
|
|
res += db.hostName();
|
|
}
|
|
|
|
if ( db.port() > 0 ) {
|
|
res += QLatin1Char(':') + QString::number( db.port() );
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
// drop a table only if it exists to prevent warnings
|
|
static void safeDropTables(QSqlDatabase db, const QStringList &tableNames)
|
|
{
|
|
QSqlQuery q(db);
|
|
QStringList dbtables = db.tables();
|
|
QSqlDriver::DbmsType dbType = getDatabaseType(db);
|
|
for (const QString &tableName : tableNames)
|
|
{
|
|
bool wasDropped = true;
|
|
QString table = tableName;
|
|
if (db.driver()->isIdentifierEscaped(table, QSqlDriver::TableName))
|
|
table = db.driver()->stripDelimiters(table, QSqlDriver::TableName);
|
|
|
|
if (dbtables.contains(table, Qt::CaseInsensitive)) {
|
|
for (const QString &table2 : dbtables.filter(table, Qt::CaseInsensitive)) {
|
|
if (table2.compare(table.section('.', -1, -1), Qt::CaseInsensitive) == 0) {
|
|
table = db.driver()->escapeIdentifier(table2, QSqlDriver::TableName);
|
|
if (dbType == QSqlDriver::PostgreSQL || dbType == QSqlDriver::MimerSQL)
|
|
wasDropped = q.exec( "drop table " + table + " cascade");
|
|
else
|
|
wasDropped = q.exec( "drop table " + table);
|
|
dbtables.removeAll(table2);
|
|
}
|
|
}
|
|
}
|
|
if (!wasDropped) {
|
|
qWarning() << dbToString(db) << "unable to drop table" << tableName << ':' << q.lastError();
|
|
// qWarning() << "last query:" << q.lastQuery();
|
|
// qWarning() << "dbtables:" << dbtables;
|
|
// qWarning() << "db.tables():" << db.tables();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void safeDropViews(QSqlDatabase db, const QStringList &viewNames)
|
|
{
|
|
if (isMSAccess(db)) // Access is sooo stupid.
|
|
safeDropTables(db, viewNames);
|
|
|
|
QSqlQuery q(db);
|
|
QStringList dbtables = db.tables(QSql::Views);
|
|
for (const QString &viewName : viewNames)
|
|
{
|
|
bool wasDropped = true;
|
|
QString view = viewName;
|
|
if (db.driver()->isIdentifierEscaped(view, QSqlDriver::TableName))
|
|
view = db.driver()->stripDelimiters(view, QSqlDriver::TableName);
|
|
|
|
if (dbtables.contains(view, Qt::CaseInsensitive)) {
|
|
for (const QString &view2 : dbtables.filter(view, Qt::CaseInsensitive)) {
|
|
if (view2.compare(view.section('.', -1, -1), Qt::CaseInsensitive) == 0) {
|
|
view = db.driver()->escapeIdentifier(view2, QSqlDriver::TableName);
|
|
wasDropped = q.exec("drop view " + view);
|
|
dbtables.removeAll(view);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!wasDropped)
|
|
qWarning() << dbToString(db) << "unable to drop view" << viewName << ':' << q.lastError();
|
|
// << "\nlast query:" << q.lastQuery()
|
|
// << "\ndbtables:" << dbtables
|
|
// << "\ndb.tables(QSql::Views):" << db.tables(QSql::Views);
|
|
}
|
|
}
|
|
|
|
// returns the type name of the blob datatype for the database db.
|
|
// blobSize is only used if the db doesn't have a generic blob type
|
|
static QString blobTypeName( QSqlDatabase db, int blobSize = 10000 )
|
|
{
|
|
const QSqlDriver::DbmsType dbType = getDatabaseType(db);
|
|
if (dbType == QSqlDriver::MySqlServer)
|
|
return "longblob";
|
|
|
|
if (dbType == QSqlDriver::PostgreSQL)
|
|
return "bytea";
|
|
|
|
if (dbType == QSqlDriver::Sybase
|
|
|| dbType == QSqlDriver::MSSqlServer
|
|
|| isMSAccess( db ) )
|
|
return "image";
|
|
|
|
if (dbType == QSqlDriver::DB2)
|
|
return QString( "blob(%1)" ).arg( blobSize );
|
|
|
|
if (dbType == QSqlDriver::Interbase)
|
|
return QString( "blob sub_type 0 segment size 4096" );
|
|
|
|
if (dbType == QSqlDriver::Oracle
|
|
|| dbType == QSqlDriver::SQLite)
|
|
return "blob";
|
|
|
|
qDebug() << "tst_Databases::blobTypeName: Don't know the blob type for" << dbToString( db );
|
|
|
|
return "blob";
|
|
}
|
|
|
|
static QString dateTimeTypeName(QSqlDatabase db)
|
|
{
|
|
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
|
|
if (dbType == QSqlDriver::PostgreSQL)
|
|
return QLatin1String("timestamptz");
|
|
if (dbType == QSqlDriver::Oracle && getOraVersion(db) >= 9)
|
|
return QLatin1String("timestamp(0)");
|
|
if (dbType == QSqlDriver::Interbase || dbType == QSqlDriver::MimerSQL)
|
|
return QLatin1String("timestamp");
|
|
return QLatin1String("datetime");
|
|
}
|
|
|
|
static QString timeTypeName(QSqlDatabase db)
|
|
{
|
|
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
|
|
if (dbType == QSqlDriver::Oracle && getOraVersion(db) >= 9)
|
|
return QLatin1String("timestamp(0)");
|
|
return QLatin1String("time");
|
|
}
|
|
|
|
static QString dateTypeName(QSqlDatabase db)
|
|
{
|
|
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
|
|
if (dbType == QSqlDriver::Oracle && getOraVersion(db) >= 9)
|
|
return QLatin1String("timestamp(0)");
|
|
return QLatin1String("date");
|
|
}
|
|
|
|
static QString autoFieldName( QSqlDatabase db )
|
|
{
|
|
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
|
|
if (dbType == QSqlDriver::MySqlServer)
|
|
return "AUTO_INCREMENT";
|
|
if (dbType == QSqlDriver::Sybase || dbType == QSqlDriver::MSSqlServer)
|
|
return "IDENTITY";
|
|
/* if (dbType == QSqlDriver::PostgreSQL)
|
|
return "SERIAL";*/
|
|
// if (dbType == QSqlDriver::DB2)
|
|
// return "GENERATED BY DEFAULT AS IDENTITY";
|
|
|
|
return QString();
|
|
}
|
|
|
|
static QByteArray printError(const QSqlError &err)
|
|
{
|
|
QString result;
|
|
if (!err.nativeErrorCode().isEmpty())
|
|
result += u'(' + err.nativeErrorCode() + ") ";
|
|
result += u'\'';
|
|
if (!err.driverText().isEmpty())
|
|
result += err.driverText() + "' || '";
|
|
result += err.databaseText() + u'\'';
|
|
return result.toLocal8Bit();
|
|
}
|
|
|
|
static QByteArray printError(const QSqlError &err, const QSqlDatabase &db)
|
|
{
|
|
return dbToString(db).toLocal8Bit() + ": " + printError(err);
|
|
}
|
|
|
|
static QSqlDriver::DbmsType getDatabaseType(QSqlDatabase db)
|
|
{
|
|
return db.driver()->dbmsType();
|
|
}
|
|
|
|
static bool isMSAccess( QSqlDatabase db )
|
|
{
|
|
return db.databaseName().contains( "Access Driver", Qt::CaseInsensitive );
|
|
}
|
|
|
|
// -1 on fail, else Oracle version
|
|
static int getOraVersion( QSqlDatabase db )
|
|
{
|
|
int ver = -1;
|
|
QSqlQuery q( "SELECT banner FROM v$version", db );
|
|
q.next();
|
|
|
|
QRegularExpression vers("([0-9]+)\\.[0-9\\.]+[0-9]");
|
|
QRegularExpressionMatch match = vers.match(q.value(0).toString());
|
|
if (match.hasMatch()) {
|
|
bool ok;
|
|
ver = match.captured(1).toInt(&ok);
|
|
|
|
if (!ok)
|
|
ver = -1;
|
|
}
|
|
|
|
return ver;
|
|
}
|
|
|
|
QStringList dbNames;
|
|
int counter = 0;
|
|
|
|
private:
|
|
QTemporaryDir *dbDir()
|
|
{
|
|
if (m_dbDir.isNull()) {
|
|
m_dbDir.reset(new QTemporaryDir);
|
|
if (!m_dbDir->isValid()) {
|
|
qWarning() << Q_FUNC_INFO << "Unable to create a temporary directory: " << QDir::toNativeSeparators(m_dbDir->path());
|
|
m_dbDir.reset();
|
|
}
|
|
}
|
|
return m_dbDir.data();
|
|
}
|
|
|
|
QScopedPointer<QTemporaryDir> m_dbDir;
|
|
};
|
|
|
|
class TableScope
|
|
{
|
|
public:
|
|
TableScope(const QSqlDatabase &db, const QString &fullTableName)
|
|
: m_db(db)
|
|
, m_tableName(fullTableName)
|
|
{
|
|
tst_Databases::safeDropTables(m_db, {m_tableName});
|
|
}
|
|
TableScope(const QSqlDatabase &db, const char *tableName, const char *file, bool escape = true)
|
|
: TableScope(db, qTableName(tableName, file, db, escape))
|
|
{
|
|
}
|
|
|
|
~TableScope()
|
|
{
|
|
tst_Databases::safeDropTables(m_db, {m_tableName});
|
|
}
|
|
|
|
QString tableName() const
|
|
{
|
|
return m_tableName;
|
|
}
|
|
private:
|
|
QSqlDatabase m_db;
|
|
QString m_tableName;
|
|
};
|
|
|
|
#endif
|
|
|