Andreas Bacher 5af5738980 SQL/IBASE: Time Zone support (firebird 4.x)
Add support for time zones in the IBASE driver, which was introduced in
firebird 4.x. TIMESTAMP WITH TIME ZONE data type is supported in order
to store and retrieve a QDateTime with the time zone.

Task-number: QTBUG-111879
Change-Id: I631b4262d17796a17630379b7d659f88244a23ad
Reviewed-by: Christian Ehrlicher <ch.ehrlicher@gmx.de>
2023-03-24 23:43:54 +01:00

499 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 <QSqlRecord>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QDir>
#include <QScopedPointer>
#include <QVariant>
#include <QDebug>
#include <QSqlTableModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QSysInfo>
#include <QVersionNumber>
#include <QtSql/private/qsqldriver_p.h>
#include <QTest>
using namespace Qt::StringLiterals;
#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"); }
// 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 hash = qHash(QLatin1String(sourceFileName) + '_' +
QSysInfo::machineHostName().replace('-', '_'));
auto tableStr = QLatin1String("dbtst") + db.driverName() + '_' + prefix +
QString::number(hash, 16);
// Oracle & Interbase/Firebird have a limit on the tablename length
QSqlDriver *drv = db.driver();
if (drv)
tableStr.truncate(drv->maximumIdentifierLength(QSqlDriver::TableName));
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 (const auto &dbName : std::as_const(dbNames)) {
QSqlDatabase db = QSqlDatabase::database(dbName);
if (!db.isValid())
continue;
if (driverPrefix.isEmpty() || db.driverName().startsWith(driverPrefix)) {
QTest::newRow(dbName.toLatin1()) << dbName;
++count;
}
}
return count;
}
int fillTestTableWithStrategies(const QString &driverPrefix = QString()) const
{
QTest::addColumn<QString>("dbName");
QTest::addColumn<QSqlTableModel::EditStrategy>("submitpolicy");
int count = 0;
for (const auto &dbName : std::as_const(dbNames)) {
QSqlDatabase db = QSqlDatabase::database(dbName);
if (!db.isValid())
continue;
if ( driverPrefix.isEmpty() || db.driverName().startsWith( driverPrefix ) ) {
QTest::newRow(QString("%1 [field]").arg(dbName).toLatin1() ) << dbName << QSqlTableModel::OnFieldChange;
QTest::newRow(QString("%1 [row]").arg(dbName).toLatin1() ) << dbName << QSqlTableModel::OnRowChange;
QTest::newRow(QString("%1 [manual]").arg(dbName).toLatin1() ) << dbName << 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())
{
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));
}
auto 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();
for (const auto &elem : entriesA) {
if (elem.isObject()) {
const QJsonObject object = elem.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;
}
}
}
}
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;
auto it = dbNames.begin();
while (it != dbNames.end()) {
const auto &dbName = *it;
QSqlDatabase db = QSqlDatabase::database(dbName, false );
qDebug() << "Opening:" << dbName;
if (db.isValid() && !db.isOpen()) {
if (!db.open()) {
qWarning("tst_Databases: Unable to open %s on %s:\n%s", qPrintable(db.driverName()),
qPrintable(dbName), 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 (const auto &dbName : std::as_const(dbNames)) {
{
QSqlDatabase db = QSqlDatabase::database(dbName, false);
if (db.isValid() && db.isOpen())
db.close();
}
QSqlDatabase::removeDatabase(dbName);
}
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(const 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(const 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(const 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(const 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(const 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(const 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(const 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(const QSqlDatabase &db)
{
return db.driver()->dbmsType();
}
static bool isMSAccess(const QSqlDatabase &db)
{
return db.databaseName().contains( "Access Driver", Qt::CaseInsensitive );
}
// -1 on fail, else Oracle version
static int getOraVersion(const 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 QVersionNumber getIbaseEngineVersion(const QSqlDatabase &db)
{
auto q = db.exec("SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') as version from rdb$database;"_L1);
q.next();
auto record = q.record();
auto version = QVersionNumber::fromString(record.value(0).toString());
return version;
}
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