summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndreas Bacher <andreas.bacher@meon-medical.com>2023-03-14 11:25:20 +0100
committerAndreas Bacher <andreas.bacher@meon-medical.com>2023-03-24 23:43:54 +0100
commit5af57389807d0b51509d55e1f87042cab393da6e (patch)
tree296d45d53137a97c616c0a20966e04086cda3325
parentdbus examples: Remove "Example" from example names (diff)
downloadqtbase-5af57389807d0b51509d55e1f87042cab393da6e.tar.xz
qtbase-5af57389807d0b51509d55e1f87042cab393da6e.zip
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>
-rw-r--r--src/plugins/sqldrivers/ibase/qsql_ibase.cpp135
-rw-r--r--tests/auto/sql/kernel/qsqldatabase/tst_databases.h13
-rw-r--r--tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp113
3 files changed, 252 insertions, 9 deletions
diff --git a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp
index 95b994e7cd..4284f9b811 100644
--- a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp
+++ b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp
@@ -4,6 +4,7 @@
#include "qsql_ibase_p.h"
#include <QtCore/qcoreapplication.h>
#include <QtCore/qdatetime.h>
+#include <QtCore/qtimezone.h>
#include <QtCore/qdeadlinetimer.h>
#include <QtCore/qdebug.h>
#include <QtCore/qlist.h>
@@ -20,6 +21,7 @@
#include <stdlib.h>
#include <limits.h>
#include <math.h>
+#include <mutex>
QT_BEGIN_NAMESPACE
@@ -38,6 +40,14 @@ using namespace Qt::StringLiterals;
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)
{
if (status[0] != 1 || status[1] <= 0)
@@ -85,6 +95,9 @@ static void initDA(XSQLDA *sqlda)
case SQL_FLOAT:
case SQL_DOUBLE:
case SQL_TIMESTAMP:
+#if (FB_API_VER >= 40)
+ case SQL_TIMESTAMP_TZ:
+#endif
case SQL_TYPE_TIME:
case SQL_TYPE_DATE:
case SQL_TEXT:
@@ -139,6 +152,9 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale)
case blr_sql_date:
return QMetaType::QDate;
case blr_timestamp:
+#if (FB_API_VER >= 40)
+ case blr_timestamp_tz:
+#endif
return QMetaType::QDateTime;
case blr_blob:
return QMetaType::QByteArray;
@@ -174,6 +190,9 @@ static QMetaType::Type qIBaseTypeName2(int iType, bool hasScale)
case SQL_DOUBLE:
return QMetaType::Double;
case SQL_TIMESTAMP:
+#if (FB_API_VER >= 40)
+ case SQL_TIMESTAMP_TZ:
+#endif
return QMetaType::QDateTime;
case SQL_TYPE_TIME:
return QMetaType::QTime;
@@ -208,12 +227,45 @@ static QDateTime fromTimeStamp(char *buffer)
// have to demangle the structure ourselves because isc_decode_time
// strips the msecs
- t = t.addMSecs(int(((ISC_TIMESTAMP*)buffer)->timestamp_time / 10));
- d = bd.addDays(int(((ISC_TIMESTAMP*)buffer)->timestamp_date));
-
+ auto timebuf = reinterpret_cast<ISC_TIMESTAMP*>(buffer);
+ t = t.addMSecs(static_cast<int>(timebuf->timestamp_time / 10));
+ d = bd.addDays(timebuf->timestamp_date);
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 const QTime midnight(0, 0, 0, 0);
@@ -282,6 +334,34 @@ public:
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:
isc_db_handle ibase;
isc_tr_handle trans;
@@ -513,8 +593,16 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim,
buffer += sizeof(ISC_TIMESTAMP);
}
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:
- for(int i = 0; i < numElements[dim]; ++i) {
+ for (int i = 0; i < numElements[dim]; ++i) {
valList.append(fromTime(buffer));
buffer += sizeof(ISC_TIME);
}
@@ -709,8 +797,20 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list,
break;
case QMetaType::QDateTime:
for (const auto &elem : list) {
- *((ISC_TIMESTAMP*)buffer) = toTimeStamp(elem.toDateTime());
- buffer += sizeof(ISC_TIMESTAMP);
+ switch (arrayDesc->array_desc_dtype) {
+ 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;
case QMetaType::Bool:
@@ -914,7 +1014,6 @@ bool QIBaseResult::prepare(const QString& query)
return true;
}
-
bool QIBaseResult::exec()
{
Q_D(QIBaseResult);
@@ -988,6 +1087,11 @@ bool QIBaseResult::exec()
case SQL_TIMESTAMP:
*((ISC_TIMESTAMP*)d->inda->sqlvar[para].sqldata) = toTimeStamp(val.toDateTime());
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:
*((ISC_TIME*)d->inda->sqlvar[para].sqldata) = toTime(val.toTime());
break;
@@ -1171,6 +1275,11 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx)
case SQL_BOOLEAN:
row[idx] = QVariant(bool((*(bool*)buf)));
break;
+#if (FB_API_VER >= 40)
+ case SQL_TIMESTAMP_TZ:
+ row[idx] = fromTimeStampTz(buf);
+ break;
+#endif
default:
// unknown type - don't even try to fetch
row[idx] = QVariant();
@@ -1469,6 +1578,14 @@ bool QIBaseDriver::open(const QString &db,
setOpen(true);
setOpenError(false);
+#if (FB_API_VER >= 40)
+ std::call_once(initTZMappingFlag, [d](){ d->initTZMappingCache(); });
+ if (lastError().isValid())
+ {
+ setOpen(true);
+ return false;
+ }
+#endif
return true;
}
@@ -1571,8 +1688,8 @@ QStringList QIBaseDriver::tables(QSql::TableType type) const
q.setForwardOnly(true);
if (!q.exec("select rdb$relation_name from rdb$relations "_L1 + typeFilter))
return res;
- while(q.next())
- res << q.value(0).toString().simplified();
+ while (q.next())
+ res << q.value(0).toString().simplified();
return res;
}
diff --git a/tests/auto/sql/kernel/qsqldatabase/tst_databases.h b/tests/auto/sql/kernel/qsqldatabase/tst_databases.h
index c715c1c2a8..c62f6a97ff 100644
--- a/tests/auto/sql/kernel/qsqldatabase/tst_databases.h
+++ b/tests/auto/sql/kernel/qsqldatabase/tst_databases.h
@@ -9,6 +9,7 @@
#include <QSqlDriver>
#include <QSqlError>
#include <QSqlQuery>
+#include <QSqlRecord>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QDir>
@@ -20,9 +21,12 @@
#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" ); }
@@ -434,6 +438,15 @@ public:
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;
diff --git a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp
index 048b9b0984..431c2f6f92 100644
--- a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp
+++ b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp
@@ -246,6 +246,11 @@ private slots:
void ibaseArray_data() { generic_data("QIBASE"); }
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
// invalidQuery() if run later; so put this one last !
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()
{
// 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());
}
+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()
{
QFETCH(QString, dbName);