// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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; } inline static QString qTableName(const QString& prefix, QSqlDatabase db) { QString tableStr; if (db.driverName().toLower().contains("ODBC")) tableStr += QLatin1String("_odbc"); return fixupTableName(QString(db.driver()->escapeIdentifier(prefix + tableStr + QLatin1Char('_') + qGetHostName(), QSqlDriver::TableName)),db); } inline static bool testWhiteSpaceNames( const QString &name ) { /* return name.startsWith( "QPSQL" ) || name.startsWith( "QODBC" ) || name.startsWith( "QSQLITE" ) || name.startsWith( "QMYSQL" );*/ return name != QLatin1String("QSQLITE2"); } inline static QString toHex( const QString& binary ) { QString str; static char const hexchars[] = "0123456789ABCDEF"; for ( int i = 0; i < binary.size(); i++ ) { ushort code = binary.at(i).unicode(); str += (QChar)(hexchars[ (code >> 12) & 0x0F ]); str += (QChar)(hexchars[ (code >> 8) & 0x0F ]); str += (QChar)(hexchars[ (code >> 4) & 0x0F ]); str += (QChar)(hexchars[ code & 0x0F ]); } return str; } class tst_Databases { public: tst_Databases(): counter( 0 ) { } ~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( "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( "dbName" ); QTest::addColumn("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 ) { bool wasDropped; QSqlQuery q( db ); QStringList dbtables=db.tables(); QSqlDriver::DbmsType dbType = getDatabaseType(db); foreach(const QString &tableName, tableNames) { 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 ) ) { foreach(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) 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 safeDropTable( QSqlDatabase db, const QString& tableName ) { safeDropTables(db, QStringList() << tableName); } static void safeDropViews( QSqlDatabase db, const QStringList &viewNames ) { if ( isMSAccess( db ) ) // Access is sooo stupid. safeDropTables( db, viewNames ); bool wasDropped; QSqlQuery q( db ); QStringList dbtables=db.tables(QSql::Views); foreach(QString viewName, viewNames) { 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 ) ) { foreach(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); } } static void safeDropView( QSqlDatabase db, const QString& tableName ) { safeDropViews(db, QStringList() << tableName); } // 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) 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 += '(' + err.nativeErrorCode() + ") "; result += '\''; if(!err.driverText().isEmpty()) result += err.driverText() + "' || '"; result += err.databaseText() + QLatin1Char('\''); return result.toLocal8Bit(); } static QByteArray printError( const QSqlError& err, const QSqlDatabase& db ) { QString result(dbToString(db) + ": "); if (!err.nativeErrorCode().isEmpty()) result += '(' + err.nativeErrorCode() + ") "; result += '\''; if(!err.driverText().isEmpty()) result += err.driverText() + "' || '"; result += err.databaseText() + QLatin1Char('\''); return result.toLocal8Bit(); } static QSqlDriver::DbmsType getDatabaseType(QSqlDatabase db) { QSqlDriverPrivate *d = static_cast(QObjectPrivate::get(db.driver())); return d->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; } static QString getMySqlVersion( const QSqlDatabase &db ) { QSqlQuery q(db); q.exec( "select version()" ); if(q.next()) return q.value( 0 ).toString(); else return QString(); } static QString getPSQLVersion( const QSqlDatabase &db ) { QSqlQuery q(db); q.exec( "select version()" ); if(q.next()) return q.value( 0 ).toString(); else return QString(); } QStringList dbNames; int counter; 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 m_dbDir; }; #endif