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>
This commit is contained in:
Andreas Bacher 2023-03-14 11:25:20 +01:00
parent 8faf2511da
commit 5af5738980
3 changed files with 252 additions and 9 deletions

View File

@ -4,6 +4,7 @@
#include "qsql_ibase_p.h" #include "qsql_ibase_p.h"
#include <QtCore/qcoreapplication.h> #include <QtCore/qcoreapplication.h>
#include <QtCore/qdatetime.h> #include <QtCore/qdatetime.h>
#include <QtCore/qtimezone.h>
#include <QtCore/qdeadlinetimer.h> #include <QtCore/qdeadlinetimer.h>
#include <QtCore/qdebug.h> #include <QtCore/qdebug.h>
#include <QtCore/qlist.h> #include <QtCore/qlist.h>
@ -20,6 +21,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <limits.h> #include <limits.h>
#include <math.h> #include <math.h>
#include <mutex>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -38,6 +40,14 @@ using namespace Qt::StringLiterals;
constexpr qsizetype QIBaseChunkSize = SHRT_MAX / 2; constexpr qsizetype QIBaseChunkSize = SHRT_MAX / 2;
#if (FB_API_VER >= 40)
typedef QMap<quint16, QByteArray> QFbTzIdToIanaIdMap;
typedef QMap<QByteArray, quint16> QIanaIdToFbTzIdMap;
Q_GLOBAL_STATIC(QFbTzIdToIanaIdMap, qFbTzIdToIanaIdMap)
Q_GLOBAL_STATIC(QIanaIdToFbTzIdMap, qIanaIdToFbTzIdMap)
std::once_flag initTZMappingFlag;
#endif
static bool getIBaseError(QString& msg, const ISC_STATUS* status, ISC_LONG &sqlcode) static bool getIBaseError(QString& msg, const ISC_STATUS* status, ISC_LONG &sqlcode)
{ {
if (status[0] != 1 || status[1] <= 0) if (status[0] != 1 || status[1] <= 0)
@ -85,6 +95,9 @@ static void initDA(XSQLDA *sqlda)
case SQL_FLOAT: case SQL_FLOAT:
case SQL_DOUBLE: case SQL_DOUBLE:
case SQL_TIMESTAMP: case SQL_TIMESTAMP:
#if (FB_API_VER >= 40)
case SQL_TIMESTAMP_TZ:
#endif
case SQL_TYPE_TIME: case SQL_TYPE_TIME:
case SQL_TYPE_DATE: case SQL_TYPE_DATE:
case SQL_TEXT: case SQL_TEXT:
@ -139,6 +152,9 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale)
case blr_sql_date: case blr_sql_date:
return QMetaType::QDate; return QMetaType::QDate;
case blr_timestamp: case blr_timestamp:
#if (FB_API_VER >= 40)
case blr_timestamp_tz:
#endif
return QMetaType::QDateTime; return QMetaType::QDateTime;
case blr_blob: case blr_blob:
return QMetaType::QByteArray; return QMetaType::QByteArray;
@ -174,6 +190,9 @@ static QMetaType::Type qIBaseTypeName2(int iType, bool hasScale)
case SQL_DOUBLE: case SQL_DOUBLE:
return QMetaType::Double; return QMetaType::Double;
case SQL_TIMESTAMP: case SQL_TIMESTAMP:
#if (FB_API_VER >= 40)
case SQL_TIMESTAMP_TZ:
#endif
return QMetaType::QDateTime; return QMetaType::QDateTime;
case SQL_TYPE_TIME: case SQL_TYPE_TIME:
return QMetaType::QTime; return QMetaType::QTime;
@ -208,12 +227,45 @@ static QDateTime fromTimeStamp(char *buffer)
// have to demangle the structure ourselves because isc_decode_time // have to demangle the structure ourselves because isc_decode_time
// strips the msecs // strips the msecs
t = t.addMSecs(int(((ISC_TIMESTAMP*)buffer)->timestamp_time / 10)); auto timebuf = reinterpret_cast<ISC_TIMESTAMP*>(buffer);
d = bd.addDays(int(((ISC_TIMESTAMP*)buffer)->timestamp_date)); t = t.addMSecs(static_cast<int>(timebuf->timestamp_time / 10));
d = bd.addDays(timebuf->timestamp_date);
return QDateTime(d, t); return QDateTime(d, t);
} }
#if (FB_API_VER >= 40)
QDateTime fromTimeStampTz(char *buffer)
{
static const QDate bd(1858, 11, 17);
QTime t(0, 0);
QDate d;
// have to demangle the structure ourselves because isc_decode_time
// strips the msecs
auto timebuf = reinterpret_cast<ISC_TIMESTAMP_TZ*>(buffer);
t = t.addMSecs(static_cast<int>(timebuf->utc_timestamp.timestamp_time / 10));
d = bd.addDays(timebuf->utc_timestamp.timestamp_date);
quint16 fpTzID = timebuf->time_zone;
QByteArray timeZoneName = qFbTzIdToIanaIdMap()->value(fpTzID);
if (!timeZoneName.isEmpty())
return QDateTime(d, t, QTimeZone(timeZoneName));
else
return {};
}
ISC_TIMESTAMP_TZ toTimeStampTz(const QDateTime &dt)
{
static const QTime midnight(0, 0, 0, 0);
static const QDate basedate(1858, 11, 17);
ISC_TIMESTAMP_TZ ts;
ts.utc_timestamp.timestamp_time = midnight.msecsTo(dt.time()) * 10;
ts.utc_timestamp.timestamp_date = basedate.daysTo(dt.date());
ts.time_zone = qIanaIdToFbTzIdMap()->value(dt.timeZone().id().simplified(), 0);
return ts;
}
#endif
static ISC_TIME toTime(QTime t) static ISC_TIME toTime(QTime t)
{ {
static const QTime midnight(0, 0, 0, 0); static const QTime midnight(0, 0, 0, 0);
@ -282,6 +334,34 @@ public:
return true; return true;
} }
#if (FB_API_VER >= 40)
void initTZMappingCache()
{
Q_Q(QIBaseDriver);
QSqlQuery qry(q->createResult());
qry.setForwardOnly(true);
qry.exec(QString("select * from RDB$TIME_ZONES"_L1));
if (qry.lastError().type()) {
q->setLastError(QSqlError(
QCoreApplication::translate("QIBaseDriver",
"failed to query time zone mapping from system table"),
qry.lastError().databaseText(),
QSqlError::StatementError,
qry.lastError().nativeErrorCode()));
return;
}
while (qry.next()) {
auto record = qry.record();
quint16 fbTzId = record.value(0).value<quint16>();
QByteArray ianaId = record.value(1).toByteArray().simplified();
qFbTzIdToIanaIdMap()->insert(fbTzId, ianaId);
qIanaIdToFbTzIdMap()->insert(ianaId, fbTzId);
}
}
#endif
public: public:
isc_db_handle ibase; isc_db_handle ibase;
isc_tr_handle trans; isc_tr_handle trans;
@ -513,8 +593,16 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim,
buffer += sizeof(ISC_TIMESTAMP); buffer += sizeof(ISC_TIMESTAMP);
} }
break; break;
#if (FB_API_VER >= 40)
case blr_timestamp_tz:
for (int i = 0; i < numElements[dim]; ++i) {
valList.append(fromTimeStampTz(buffer));
buffer += sizeof(ISC_TIMESTAMP_TZ);
}
break;
#endif
case blr_sql_time: case blr_sql_time:
for(int i = 0; i < numElements[dim]; ++i) { for (int i = 0; i < numElements[dim]; ++i) {
valList.append(fromTime(buffer)); valList.append(fromTime(buffer));
buffer += sizeof(ISC_TIME); buffer += sizeof(ISC_TIME);
} }
@ -709,8 +797,20 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list,
break; break;
case QMetaType::QDateTime: case QMetaType::QDateTime:
for (const auto &elem : list) { for (const auto &elem : list) {
*((ISC_TIMESTAMP*)buffer) = toTimeStamp(elem.toDateTime()); switch (arrayDesc->array_desc_dtype) {
buffer += sizeof(ISC_TIMESTAMP); case blr_timestamp:
*((ISC_TIMESTAMP*)buffer) = toTimeStamp(elem.toDateTime());
buffer += sizeof(ISC_TIMESTAMP);
break;
#if (FB_API_VER >= 40)
case blr_timestamp_tz:
*((ISC_TIMESTAMP_TZ*)buffer) = toTimeStampTz(elem.toDateTime());
buffer += sizeof(ISC_TIMESTAMP_TZ);
break;
#endif
default:
break;
}
} }
break; break;
case QMetaType::Bool: case QMetaType::Bool:
@ -914,7 +1014,6 @@ bool QIBaseResult::prepare(const QString& query)
return true; return true;
} }
bool QIBaseResult::exec() bool QIBaseResult::exec()
{ {
Q_D(QIBaseResult); Q_D(QIBaseResult);
@ -988,6 +1087,11 @@ bool QIBaseResult::exec()
case SQL_TIMESTAMP: case SQL_TIMESTAMP:
*((ISC_TIMESTAMP*)d->inda->sqlvar[para].sqldata) = toTimeStamp(val.toDateTime()); *((ISC_TIMESTAMP*)d->inda->sqlvar[para].sqldata) = toTimeStamp(val.toDateTime());
break; break;
#if (FB_API_VER >= 40)
case SQL_TIMESTAMP_TZ:
*((ISC_TIMESTAMP_TZ*)d->inda->sqlvar[para].sqldata) = toTimeStampTz(val.toDateTime());
break;
#endif
case SQL_TYPE_TIME: case SQL_TYPE_TIME:
*((ISC_TIME*)d->inda->sqlvar[para].sqldata) = toTime(val.toTime()); *((ISC_TIME*)d->inda->sqlvar[para].sqldata) = toTime(val.toTime());
break; break;
@ -1171,6 +1275,11 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx)
case SQL_BOOLEAN: case SQL_BOOLEAN:
row[idx] = QVariant(bool((*(bool*)buf))); row[idx] = QVariant(bool((*(bool*)buf)));
break; break;
#if (FB_API_VER >= 40)
case SQL_TIMESTAMP_TZ:
row[idx] = fromTimeStampTz(buf);
break;
#endif
default: default:
// unknown type - don't even try to fetch // unknown type - don't even try to fetch
row[idx] = QVariant(); row[idx] = QVariant();
@ -1469,6 +1578,14 @@ bool QIBaseDriver::open(const QString &db,
setOpen(true); setOpen(true);
setOpenError(false); setOpenError(false);
#if (FB_API_VER >= 40)
std::call_once(initTZMappingFlag, [d](){ d->initTZMappingCache(); });
if (lastError().isValid())
{
setOpen(true);
return false;
}
#endif
return true; return true;
} }
@ -1571,8 +1688,8 @@ QStringList QIBaseDriver::tables(QSql::TableType type) const
q.setForwardOnly(true); q.setForwardOnly(true);
if (!q.exec("select rdb$relation_name from rdb$relations "_L1 + typeFilter)) if (!q.exec("select rdb$relation_name from rdb$relations "_L1 + typeFilter))
return res; return res;
while(q.next()) while (q.next())
res << q.value(0).toString().simplified(); res << q.value(0).toString().simplified();
return res; return res;
} }

View File

@ -9,6 +9,7 @@
#include <QSqlDriver> #include <QSqlDriver>
#include <QSqlError> #include <QSqlError>
#include <QSqlQuery> #include <QSqlQuery>
#include <QSqlRecord>
#include <QRegularExpression> #include <QRegularExpression>
#include <QRegularExpressionMatch> #include <QRegularExpressionMatch>
#include <QDir> #include <QDir>
@ -20,9 +21,12 @@
#include <QJsonObject> #include <QJsonObject>
#include <QJsonDocument> #include <QJsonDocument>
#include <QSysInfo> #include <QSysInfo>
#include <QVersionNumber>
#include <QtSql/private/qsqldriver_p.h> #include <QtSql/private/qsqldriver_p.h>
#include <QTest> #include <QTest>
using namespace Qt::StringLiterals;
#define CHECK_DATABASE( db ) \ #define CHECK_DATABASE( db ) \
if ( !db.isValid() ) { qFatal( "db is Invalid" ); } if ( !db.isValid() ) { qFatal( "db is Invalid" ); }
@ -434,6 +438,15 @@ public:
return ver; 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; QStringList dbNames;
int counter = 0; int counter = 0;

View File

@ -246,6 +246,11 @@ private slots:
void ibaseArray_data() { generic_data("QIBASE"); } void ibaseArray_data() { generic_data("QIBASE"); }
void ibaseArray(); void ibaseArray();
void ibaseDateTimeWithTZ_data();
void ibaseDateTimeWithTZ();
void ibaseTimeStampTzArray_data() { generic_data("QIBASE"); }
void ibaseTimeStampTzArray();
// Double addDatabase() with same name leaves system in a state that breaks // Double addDatabase() with same name leaves system in a state that breaks
// invalidQuery() if run later; so put this one last ! // invalidQuery() if run later; so put this one last !
void prematureExec_data() { generic_data(); } void prematureExec_data() { generic_data(); }
@ -4818,6 +4823,79 @@ void tst_QSqlQuery::dateTime()
} }
} }
void tst_QSqlQuery::ibaseDateTimeWithTZ_data()
{
if (dbs.dbNames.isEmpty())
QSKIP("No database drivers are available in this Qt configuration");
QTest::addColumn<QString>("dbName");
QTest::addColumn<QString>("tableName");
QTest::addColumn<QList<QDateTime> >("initialDateTimes");
QTest::addColumn<QList<QDateTime> >("expectedDateTimes");
const QTimeZone afterUTCTimeZone("Asia/Hong_Kong");
const QTimeZone beforeUTCTimeZone("America/Los_Angeles");
const QTimeZone utcTimeZone("UTC");
const QDateTime dtWithAfterTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), afterUTCTimeZone);
const QDateTime dtWithBeforeTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), beforeUTCTimeZone);
const QDateTime dtWithUTCTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), utcTimeZone);
const QDateTime dtLocalTZ(QDateTime::currentDateTime());
const QList<QDateTime> dateTimes = {
dtWithAfterTZ,
dtWithBeforeTZ,
dtWithUTCTZ,
dtLocalTZ
};
for (const QString &dbName : std::as_const(dbs.dbNames)) {
QSqlDatabase db = QSqlDatabase::database(dbName);
if (!db.isValid())
continue;
const QString tableNameTSWithTimeZone(qTableName("dateTimeTSWithTZ", __FILE__, db));
QTest::newRow(QString(dbName + " timestamp with time zone").toLatin1())
<< dbName
<< tableNameTSWithTimeZone
<< dateTimes
<< dateTimes;
}
}
void tst_QSqlQuery::ibaseDateTimeWithTZ()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
if (tst_Databases::getDatabaseType(db) != QSqlDriver::Interbase)
QSKIP("Implemented only for Interbase");
if (tst_Databases::getIbaseEngineVersion(db).majorVersion() < 4)
QSKIP("Time zone support only implemented for firebird engine version 4 and greater");
QFETCH(QString, tableName);
TableScope ts(db, tableName);
QSqlQuery q(db);
QVERIFY_SQL(q, exec(QString("CREATE TABLE " + tableName + "(dt timestamp with time zone)")));
QFETCH(QList<QDateTime>, initialDateTimes);
QFETCH(QList<QDateTime>, expectedDateTimes);
for (const QDateTime &dt : std::as_const(initialDateTimes)) {
QVERIFY_SQL(q, prepare(QLatin1String("INSERT INTO %1 values(:dt)").arg(tableName)));
q.bindValue(":dt", dt);
QVERIFY_SQL(q, exec());
}
QVERIFY_SQL(q, exec("SELECT * FROM " + tableName));
for (const QDateTime &dt : std::as_const(expectedDateTimes)) {
QVERIFY(q.next());
QCOMPARE(q.value(0).toDateTime(), dt);
}
}
void tst_QSqlQuery::sqliteVirtualTable() void tst_QSqlQuery::sqliteVirtualTable()
{ {
// Virtual tables can behave differently when it comes to prepared // Virtual tables can behave differently when it comes to prepared
@ -4953,6 +5031,41 @@ void tst_QSqlQuery::ibaseArray()
QCOMPARE(qry.value(3).toList(), boolArray.toList()); QCOMPARE(qry.value(3).toList(), boolArray.toList());
} }
void tst_QSqlQuery::ibaseTimeStampTzArray()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
if (tst_Databases::getIbaseEngineVersion(db).majorVersion() < 4)
QSKIP("Time zone support only implemented for firebird engine version 4 and greater");
TableScope ts(db, "ibasetstzarray", __FILE__);
QSqlQuery qry(db);
QVERIFY_SQL(qry, exec(QLatin1String(
"create table %1 (timeStampData timestamp with time zone[0:4])").arg(ts.tableName())));
QVERIFY_SQL(qry, prepare(QLatin1String("insert into %1 (timeStampData)"
" values(?)").arg(ts.tableName())));
const QDateTime dtWithAfterTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), QTimeZone("Asia/Hong_Kong"));
const QDateTime dtWithBeforeTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), QTimeZone("America/Los_Angeles"));
const QDateTime dtWithUTCTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), QTimeZone("UTC"));
const QDateTime dtLocalTZ(QDateTime::currentDateTime());
const QDateTime dtWithMETTZ(QDate(2015, 5, 18), QTime(4, 26, 30, 500), QTimeZone("MET"));
const auto timeStampData = QVariant{QVariantList{dtWithAfterTZ,
dtWithBeforeTZ,
dtWithUTCTZ,
dtLocalTZ,
dtWithMETTZ}};
qry.bindValue(0, timeStampData);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, exec("select * from " + ts.tableName()));
QVERIFY(qry.next());
QCOMPARE(qry.value(0).toList(), timeStampData.toList());
}
void tst_QSqlQuery::ibase_executeBlock() void tst_QSqlQuery::ibase_executeBlock()
{ {
QFETCH(QString, dbName); QFETCH(QString, dbName);