diff options
author | Andy Shaw <andy.shaw@qt.io> | 2017-09-06 01:40:24 +0200 |
---|---|---|
committer | Andy Shaw <andy.shaw@qt.io> | 2017-10-10 08:56:32 +0000 |
commit | 0900cf3581be3ff2b2e924ce0d845566c5df841e (patch) | |
tree | 6bb0606b3c1e7584ba010beff39f02ddfbed2da6 | |
parent | bbcc2a92d029ea344390466d278b3415d8b8a202 (diff) |
OCI: Use TIMESTAMP for datetime data
The TIMESTAMP type has been available in Oracle since Oracle 9i
which was released in June 2001 and contains more data than the
DATE type so it can be reliably used for the related data types.
This adds support for preserving milliseconds and the time zone
information if this is passed or in the database.
[ChangeLog][QtSql][OCI] Added support for the TIMESTAMP data type.
Task-number: QTBUG-23
Change-Id: Icf7a012dda75fb342ce6c6aa34eaa2a52755ff2d
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
-rw-r--r-- | src/plugins/sqldrivers/oci/qsql_oci.cpp | 207 | ||||
-rw-r--r-- | tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp | 157 |
2 files changed, 266 insertions, 98 deletions
diff --git a/src/plugins/sqldrivers/oci/qsql_oci.cpp b/src/plugins/sqldrivers/oci/qsql_oci.cpp index b6c74b355d..de47df10ce 100644 --- a/src/plugins/sqldrivers/oci/qsql_oci.cpp +++ b/src/plugins/sqldrivers/oci/qsql_oci.cpp @@ -55,6 +55,7 @@ #include <qvarlengtharray.h> #include <qvector.h> #include <qdebug.h> +#include <qtimezone.h> // This is needed for oracle oci when compiling with mingw-w64 headers #if defined(__MINGW64_VERSION_MAJOR) && defined(_WIN64) @@ -112,9 +113,6 @@ static const ub2 qOraCharset = OCI_UCS2ID; typedef QVarLengthArray<sb2, 32> IndicatorArray; typedef QVarLengthArray<ub2, 32> SizeArray; -static QByteArray qMakeOraDate(const QDateTime& dt); -static QDateTime qMakeDate(const char* oraDate); - static QByteArray qMakeOCINumber(const qlonglong &ll, OCIError *err); static QByteArray qMakeOCINumber(const qulonglong& ull, OCIError* err); @@ -156,6 +154,60 @@ QOCIRowId::~QOCIRowId() OCIDescriptorFree(id, OCI_DTYPE_ROWID); } +class QOCIDateTime +{ +public: + QOCIDateTime(OCIEnv *env, OCIError *err, const QDateTime &dt = QDateTime()); + ~QOCIDateTime(); + OCIDateTime *dateTime; + static QDateTime fromOCIDateTime(OCIEnv *env, OCIError *err, OCIDateTime *dt); +}; + +QOCIDateTime::QOCIDateTime(OCIEnv *env, OCIError *err, const QDateTime &dt) + : dateTime(nullptr) +{ + OCIDescriptorAlloc(env, reinterpret_cast<void**>(&dateTime), OCI_DTYPE_TIMESTAMP_TZ, 0, 0); + if (dt.isValid()) { + const QDate date = dt.date(); + const QTime time = dt.time(); + // Zone in +hh:mm format (stripping UTC prefix from OffsetName) + QString timeZone = dt.timeZone().displayName(dt, QTimeZone::OffsetName).mid(3); + const OraText *tz = reinterpret_cast<const OraText *>(timeZone.utf16()); + OCIDateTimeConstruct(env, err, dateTime, date.year(), date.month(), date.day(), time.hour(), + time.minute(), time.second(), time.msec() * 1000000, + const_cast<OraText *>(tz), timeZone.length() * sizeof(QChar)); + } +} + +QOCIDateTime::~QOCIDateTime() +{ + if (dateTime != nullptr) + OCIDescriptorFree(dateTime, OCI_DTYPE_TIMESTAMP_TZ); +} + +QDateTime QOCIDateTime::fromOCIDateTime(OCIEnv *env, OCIError *err, OCIDateTime *dateTime) +{ + sb2 year; + ub1 month, day, hour, minute, second; + ub4 nsec; + sb1 tzHour, tzMinute; + + OCIDateTimeGetDate(env, err, dateTime, &year, &month, &day); + OCIDateTimeGetTime(env, err, dateTime, &hour, &minute, &second, &nsec); + OCIDateTimeGetTimeZoneOffset(env, err, dateTime, &tzHour, &tzMinute); + int secondsOffset = (qAbs(tzHour) * 60 + tzMinute) * 60; + if (tzHour < 0) + secondsOffset = -secondsOffset; + // OCIDateTimeGetTime gives "fractions of second" as nanoseconds + return QDateTime(QDate(year, month, day), QTime(hour, minute, second, nsec / 1000000), + Qt::OffsetFromUTC, secondsOffset); +} + +struct TempStorage { + QList<QByteArray> rawData; + QList<QOCIDateTime *> dateTimes; +}; + typedef QSharedDataPointer<QOCIRowId> QOCIRowIdPointer; QT_BEGIN_INCLUDE_NAMESPACE Q_DECLARE_METATYPE(QOCIRowIdPointer) @@ -227,11 +279,11 @@ public: void setStatementAttributes(); int bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, int pos, - const QVariant &val, dvoid *indPtr, ub2 *tmpSize, QList<QByteArray> &tmpStorage); + const QVariant &val, dvoid *indPtr, ub2 *tmpSize, TempStorage &tmpStorage); int bindValues(QVector<QVariant> &values, IndicatorArray &indicators, SizeArray &tmpSizes, - QList<QByteArray> &tmpStorage); + TempStorage &tmpStorage); void outValues(QVector<QVariant> &values, IndicatorArray &indicators, - QList<QByteArray> &tmpStorage); + TempStorage &tmpStorage); inline bool isOutValue(int i) const { Q_Q(const QOCIResult); return q->bindValueType(i) & QSql::Out; } inline bool isBinaryValue(int i) const @@ -304,7 +356,7 @@ void QOCIResultPrivate::setStatementAttributes() } int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, int pos, - const QVariant &val, dvoid *indPtr, ub2 *tmpSize, QList<QByteArray> &tmpStorage) + const QVariant &val, dvoid *indPtr, ub2 *tmpSize, TempStorage &tmpStorage) { int r = OCI_SUCCESS; void *data = const_cast<void *>(val.constData()); @@ -322,14 +374,15 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in case QVariant::Time: case QVariant::Date: case QVariant::DateTime: { - QByteArray ba = qMakeOraDate(val.toDateTime()); + QOCIDateTime *ptr = new QOCIDateTime(env, err, val.toDateTime()); r = OCIBindByPos(sql, hbnd, err, pos + 1, - ba.data(), - ba.size(), - SQLT_DAT, indPtr, 0, 0, 0, 0, OCI_DEFAULT); - tmpStorage.append(ba); - break; } + &ptr->dateTime, + sizeof(OCIDateTime *), + SQLT_TIMESTAMP_TZ, indPtr, 0, 0, 0, 0, OCI_DEFAULT); + tmpStorage.dateTimes.append(ptr); + break; + } case QVariant::Int: r = OCIBindByPos(sql, hbnd, err, pos + 1, @@ -356,7 +409,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in ba.data(), ba.size(), SQLT_VNU, indPtr, 0, 0, 0, 0, OCI_DEFAULT); - tmpStorage.append(ba); + tmpStorage.rawData.append(ba); break; } case QVariant::ULongLong: @@ -367,7 +420,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in ba.data(), ba.size(), SQLT_VNU, indPtr, 0, 0, 0, 0, OCI_DEFAULT); - tmpStorage.append(ba); + tmpStorage.rawData.append(ba); break; } case QVariant::Double: @@ -437,7 +490,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in } if (r == OCI_SUCCESS) setCharset(*hbnd, OCI_HTYPE_BIND); - tmpStorage.append(ba); + tmpStorage.rawData.append(ba); break; } // default case } // switch @@ -447,7 +500,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in } int QOCIResultPrivate::bindValues(QVector<QVariant> &values, IndicatorArray &indicators, - SizeArray &tmpSizes, QList<QByteArray> &tmpStorage) + SizeArray &tmpSizes, TempStorage &tmpStorage) { int r = OCI_SUCCESS; for (int i = 0; i < values.count(); ++i) { @@ -465,27 +518,30 @@ int QOCIResultPrivate::bindValues(QVector<QVariant> &values, IndicatorArray &ind } // will assign out value and remove its temp storage. -static void qOraOutValue(QVariant &value, QList<QByteArray> &storage, OCIError* err) +static void qOraOutValue(QVariant &value, TempStorage &tmpStorage, OCIEnv *env, OCIError* err) { switch (value.type()) { case QVariant::Time: - value = qMakeDate(storage.takeFirst()).time(); + value = QOCIDateTime::fromOCIDateTime(env, err, + tmpStorage.dateTimes.takeFirst()->dateTime).time(); break; case QVariant::Date: - value = qMakeDate(storage.takeFirst()).date(); + value = QOCIDateTime::fromOCIDateTime(env, err, + tmpStorage.dateTimes.takeFirst()->dateTime).date(); break; case QVariant::DateTime: - value = qMakeDate(storage.takeFirst()); + value = QOCIDateTime::fromOCIDateTime(env, err, + tmpStorage.dateTimes.takeFirst()->dateTime); break; case QVariant::LongLong: - value = qMakeLongLong(storage.takeFirst(), err); + value = qMakeLongLong(tmpStorage.rawData.takeFirst(), err); break; case QVariant::ULongLong: - value = qMakeULongLong(storage.takeFirst(), err); + value = qMakeULongLong(tmpStorage.rawData.takeFirst(), err); break; case QVariant::String: value = QString( - reinterpret_cast<const QChar *>(storage.takeFirst().constData())); + reinterpret_cast<const QChar *>(tmpStorage.rawData.takeFirst().constData())); break; default: break; //nothing @@ -493,14 +549,14 @@ static void qOraOutValue(QVariant &value, QList<QByteArray> &storage, OCIError* } void QOCIResultPrivate::outValues(QVector<QVariant> &values, IndicatorArray &indicators, - QList<QByteArray> &tmpStorage) + TempStorage &tmpStorage) { for (int i = 0; i < values.count(); ++i) { if (!isOutValue(i)) continue; - qOraOutValue(values[i], tmpStorage, err); + qOraOutValue(values[i], tmpStorage, env, err); QVariant::Type typ = values.at(i).type(); if (indicators[i] == -1) // NULL @@ -692,11 +748,9 @@ QVariant::Type qDecodeOCIType(int ocitype, QSql::NumericalPrecisionPolicy precis break; case SQLT_DAT: case SQLT_ODT: -#ifdef SQLT_TIMESTAMP case SQLT_TIMESTAMP: case SQLT_TIMESTAMP_TZ: case SQLT_TIMESTAMP_LTZ: -#endif type = QVariant::DateTime; break; default: @@ -723,27 +777,6 @@ static QSqlField qFromOraInf(const OraFieldInfo &ofi) } /*! - \internal - - Convert QDateTime to the internal Oracle DATE format NB! - It does not handle BCE dates. -*/ -QByteArray qMakeOraDate(const QDateTime& dt) -{ - QByteArray ba; - ba.resize(7); - int year = dt.date().year(); - ba[0]= (year / 100) + 100; // century - ba[1]= (year % 100) + 100; // year - ba[2]= dt.date().month(); - ba[3]= dt.date().day(); - ba[4]= dt.time().hour() + 1; - ba[5]= dt.time().minute() + 1; - ba[6]= dt.time().second() + 1; - return ba; -} - -/*! \internal Convert qlonglong to the internal Oracle OCINumber format. @@ -793,22 +826,6 @@ qulonglong qMakeULongLong(const char* ociNumber, OCIError* err) return qull; } -QDateTime qMakeDate(const char* oraDate) -{ - int century = uchar(oraDate[0]); - if(century >= 100){ - int year = uchar(oraDate[1]); - year = ((century-100)*100) + (year-100); - int month = oraDate[2]; - int day = oraDate[3]; - int hour = oraDate[4] - 1; - int min = oraDate[5] - 1; - int sec = oraDate[6] - 1; - return QDateTime(QDate(year,month,day), QTime(hour,min,sec)); - } - return QDateTime(); -} - class QOCICols { public: @@ -831,7 +848,7 @@ private: class OraFieldInf { public: - OraFieldInf(): data(0), len(0), ind(0), typ(QVariant::Invalid), oraType(0), def(0), lob(0) + OraFieldInf() : data(0), len(0), ind(0), typ(QVariant::Invalid), oraType(0), def(0), lob(0), dataPtr(nullptr) {} ~OraFieldInf(); char *data; @@ -841,6 +858,7 @@ private: ub4 oraType; OCIDefine *def; OCILobLocator *lob; + void *dataPtr; }; QVector<OraFieldInf> fieldInf; @@ -855,6 +873,20 @@ QOCICols::OraFieldInf::~OraFieldInf() if (r != 0) qWarning("QOCICols: Cannot free LOB descriptor"); } + if (dataPtr) { + switch (typ) { + case QVariant::Date: + case QVariant::Time: + case QVariant::DateTime: { + int r = OCIDescriptorFree(dataPtr, OCI_DTYPE_TIMESTAMP_TZ); + if (r != OCI_SUCCESS) + qWarning("QOCICols: Cannot free OCIDateTime descriptor"); + break; + } + default: + break; + } + } } QOCICols::QOCICols(int size, QOCIResultPrivate* dp) @@ -901,13 +933,18 @@ QOCICols::QOCICols(int size, QOCIResultPrivate* dp) switch (ofi.type) { case QVariant::DateTime: + r = OCIDescriptorAlloc(d->env, (void **)&fieldInf[idx].dataPtr, OCI_DTYPE_TIMESTAMP_TZ, 0, 0); + if (r != OCI_SUCCESS) { + qWarning("QOCICols: Unable to allocate the OCIDateTime descriptor"); + break; + } r = OCIDefineByPos(d->sql, &dfn, d->err, count, - create(idx, dataSize+1), - dataSize+1, - SQLT_DAT, + &fieldInf[idx].dataPtr, + sizeof(OCIDateTime *), + SQLT_TIMESTAMP_TZ, &(fieldInf[idx].ind), 0, 0, OCI_DEFAULT); break; @@ -1322,11 +1359,10 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b fieldTypes.append(tp == QVariant::List ? boundValues.at(i).toList().value(0).type() : tp); } - - QList<QByteArray> tmpStorage; SizeArray tmpSizes(columnCount); QVector<QOCIBatchColumn> columns(columnCount); QOCIBatchCleanupHandler cleaner(columns); + TempStorage tmpStorage; // figuring out buffer sizes for (i = 0; i < columnCount; ++i) { @@ -1363,8 +1399,8 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b case QVariant::Time: case QVariant::Date: case QVariant::DateTime: - col.bindAs = SQLT_DAT; - col.maxLen = 7; + col.bindAs = SQLT_TIMESTAMP_TZ; + col.maxLen = sizeof(OCIDateTime *); break; case QVariant::Int: @@ -1432,7 +1468,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b for (uint row = 0; row < col.recordCount; ++row) { const QVariant &val = boundValues.at(i).toList().at(row); - if (val.isNull()){ + if (val.isNull() && !d->isOutValue(i)) { columns[i].indicators[row] = -1; columns[i].lengths[row] = 0; } else { @@ -1443,9 +1479,8 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b case QVariant::Date: case QVariant::DateTime:{ columns[i].lengths[row] = columns[i].maxLen; - const QByteArray ba = qMakeOraDate(val.toDateTime()); - Q_ASSERT(ba.size() == int(columns[i].maxLen)); - memcpy(dataPtr, ba.constData(), columns[i].maxLen); + QOCIDateTime *date = new QOCIDateTime(d->env, d->err, val.toDateTime()); + *reinterpret_cast<OCIDateTime**>(dataPtr) = date->dateTime; break; } case QVariant::Int: @@ -1581,7 +1616,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b QVariant::Type tp = boundValues.at(i).type(); if (tp != QVariant::List) { - qOraOutValue(boundValues[i], tmpStorage, d->err); + qOraOutValue(boundValues[i], tmpStorage, d->env, d->err); if (*columns[i].indicators == -1) boundValues[i] = QVariant(tp); continue; @@ -1593,16 +1628,16 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b for (uint r = 0; r < columns[i].recordCount; ++r){ if (columns[i].indicators[r] == -1) { - (*list)[r] = QVariant(); + (*list)[r] = QVariant(fieldTypes[i]); continue; } switch(columns[i].bindAs) { - case SQLT_DAT: - (*list)[r] = qMakeDate(data + r * columns[i].maxLen); + case SQLT_TIMESTAMP_TZ: + (*list)[r] = QOCIDateTime::fromOCIDateTime(d->env, d->err, + *reinterpret_cast<OCIDateTime **>(data + r * columns[i].maxLen)); break; - case SQLT_INT: (*list)[r] = *reinterpret_cast<int*>(data + r * columns[i].maxLen); break; @@ -1646,6 +1681,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVector<QVariant> &boundValues, b d->q_func()->setAt(QSql::BeforeFirstRow); d->q_func()->setActive(true); + qDeleteAll(tmpStorage.dateTimes); return true; } @@ -1754,7 +1790,8 @@ void QOCICols::getValues(QVector<QVariant> &v, int index) switch (fld.typ) { case QVariant::DateTime: - v[index + i] = QVariant(qMakeDate(fld.data)); + v[index + i] = QVariant(QOCIDateTime::fromOCIDateTime(d->env, d->err, + reinterpret_cast<OCIDateTime *>(fld.dataPtr))); break; case QVariant::Double: case QVariant::Int: @@ -1984,7 +2021,7 @@ bool QOCIResult::exec() ub2 stmtType=0; ub4 iters; ub4 mode; - QList<QByteArray> tmpStorage; + TempStorage tmpStorage; IndicatorArray indicators(boundValueCount()); SizeArray tmpSizes(boundValueCount()); @@ -2055,7 +2092,7 @@ bool QOCIResult::exec() if (hasOutValues()) d->outValues(boundValues(), indicators, tmpStorage); - + qDeleteAll(tmpStorage.dateTimes); return true; } diff --git a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp index 92e4b580e7..2a27f24314 100644 --- a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp +++ b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp @@ -248,6 +248,9 @@ private slots: void QTBUG_57138_data() { generic_data("QSQLITE"); } void QTBUG_57138(); + void dateTime_data(); + void dateTime(); + private: // returns all database connections void generic_data(const QString &engine=QString()); @@ -724,6 +727,30 @@ void tst_QSqlQuery::oraOutValues() q.addBindValue( out, QSql::Out ); QVERIFY_SQL( q, exec() ); QCOMPARE( q.boundValue( 1 ).toString(), QString( "fifibubulalakikikokololo" ) ); + + /*** in/outvalue date ***/ + QVERIFY_SQL(q, exec("create or replace procedure " + tst_outValues + "(x in date, y out date) is\n" + "begin\n" + " y := x;\n" + "end;\n")); + QVERIFY(q.prepare("call " + tst_outValues + "(?, ?)")); + const QDate date = QDate::currentDate(); + q.addBindValue(date, QSql::In); + q.addBindValue(QVariant(QDate()), QSql::Out); + QVERIFY_SQL(q, exec()); + QCOMPARE(q.boundValue(1).toDate(), date); + + /*** in/outvalue timestamp ***/ + QVERIFY_SQL(q, exec("create or replace procedure " + tst_outValues + "(x in timestamp, y out timestamp) is\n" + "begin\n" + " y := x;\n" + "end;\n")); + QVERIFY(q.prepare("call " + tst_outValues + "(?, ?)")); + const QDateTime dt = QDateTime::currentDateTime(); + q.addBindValue(dt, QSql::In); + q.addBindValue(QVariant(QVariant::DateTime), QSql::Out); + QVERIFY_SQL(q, exec()); + QCOMPARE(q.boundValue(1).toDateTime(), dt); } void tst_QSqlQuery::oraClob() @@ -2138,8 +2165,8 @@ void tst_QSqlQuery::batchExec() QSqlQuery q( db ); const QString tableName = qTableName("qtest_batch", __FILE__, db); - QVERIFY_SQL( q, exec( "create table " + tableName + " (id int, name varchar(20), dt date, num numeric(8, 4))" ) ); - QVERIFY_SQL( q, prepare( "insert into " + tableName + " (id, name, dt, num) values (?, ?, ?, ?)" ) ); + QVERIFY_SQL(q, exec("create table " + tableName + " (id int, name varchar(20), dt date, num numeric(8, 4), dtstamp TIMESTAMP(3))")); + QVERIFY_SQL(q, prepare("insert into " + tableName + " (id, name, dt, num, dtstamp) values (?, ?, ?, ?, ?)")); QVariantList intCol; intCol << 1 << 2 << QVariant( QVariant::Int ); @@ -2154,31 +2181,54 @@ void tst_QSqlQuery::batchExec() QVariantList numCol; numCol << 2.3 << 3.4 << QVariant( QVariant::Double ); - q.addBindValue( intCol ); + QVariantList timeStampCol; + const QDateTime dtStamp = QDateTime(QDate::currentDate(), QTime(1, 2, 3, 4)); + timeStampCol << dtStamp << dtStamp.addDays(-1) << QVariant(QVariant::DateTime); + + q.addBindValue(intCol); q.addBindValue( charCol ); q.addBindValue( dateCol ); q.addBindValue( numCol ); + q.addBindValue(timeStampCol); QVERIFY_SQL( q, execBatch() ); - QVERIFY_SQL( q, exec( "select id, name, dt, num from " + tableName + " order by id" ) ); + QVERIFY_SQL(q, exec("select id, name, dt, num, dtstamp from " + tableName + " order by id")); QVERIFY( q.next() ); QCOMPARE( q.value( 0 ).toInt(), 1 ); QCOMPARE( q.value( 1 ).toString(), QString( "harald" ) ); QCOMPARE( q.value( 2 ).toDateTime(), dt ); QCOMPARE( q.value( 3 ).toDouble(), 2.3 ); + QCOMPARE(q.value(4).toDateTime(), dtStamp); QVERIFY( q.next() ); QCOMPARE( q.value( 0 ).toInt(), 2 ); QCOMPARE( q.value( 1 ).toString(), QString( "boris" ) ); QCOMPARE( q.value( 2 ).toDateTime(), dt.addDays( -1 ) ); QCOMPARE( q.value( 3 ).toDouble(), 3.4 ); + QCOMPARE(q.value(4).toDateTime(), dtStamp.addDays(-1)); QVERIFY( q.next() ); QVERIFY( q.value( 0 ).isNull() ); QVERIFY( q.value( 1 ).isNull() ); QVERIFY( q.value( 2 ).isNull() ); QVERIFY( q.value( 3 ).isNull() ); + QVERIFY(q.value(4).isNull()); + + const QString procName = qTableName("qtest_batch_proc", __FILE__, db); + QVERIFY_SQL(q, exec("create or replace procedure " + procName + " (x in timestamp, y out timestamp) is\n" + "begin\n" + " y := x;\n" + "end;\n")); + QVERIFY(q.prepare("call " + procName + "(?, ?)")); + q.addBindValue(timeStampCol, QSql::In); + QVariantList emptyDateTimes; + emptyDateTimes.reserve(timeStampCol.size()); + for (int i = 0; i < timeStampCol.size(); i++) + emptyDateTimes << QVariant(QDateTime()); + q.addBindValue(emptyDateTimes, QSql::Out); + QVERIFY_SQL(q, execBatch()); + QCOMPARE(q.boundValue(1).toList(), timeStampCol); } void tst_QSqlQuery::QTBUG_43874() @@ -2894,16 +2944,14 @@ void tst_QSqlQuery::timeStampParsing() QFETCH(QString, dbName); QSqlDatabase db = QSqlDatabase::database(dbName); CHECK_DATABASE(db); + const QString tableName(qTableName("timeStampParsing", __FILE__, db)); + tst_Databases::safeDropTable(db, tableName); QSqlQuery q(db); - QVERIFY_SQL(q, exec( - "CREATE TABLE \"main\".\"datetest\" (" - "\"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT," - "\"datefield\" timestamp);" - )); - QVERIFY_SQL(q, exec( - "INSERT INTO datetest (datefield) VALUES (current_timestamp);" - )); - QVERIFY_SQL(q, exec("SELECT * FROM datetest;")); + QVERIFY_SQL(q, exec(QStringLiteral("CREATE TABLE ") + tableName + + QStringLiteral(" (id integer, datefield timestamp)"))); + QVERIFY_SQL(q, exec(QStringLiteral("INSERT INTO ") + tableName + + QStringLiteral(" (datefield) VALUES (current_timestamp)"))); + QVERIFY_SQL(q, exec(QStringLiteral("SELECT * FROM ") + tableName)); while (q.next()) QVERIFY(q.value(1).toDateTime().isValid()); } @@ -4142,5 +4190,88 @@ void tst_QSqlQuery::QTBUG_57138() QCOMPARE(q.value(2).toDateTime(), tzoffset); } +void tst_QSqlQuery::dateTime_data() +{ + QTest::addColumn<QString>("dbName"); + QTest::addColumn<QString>("tableName"); + QTest::addColumn<QString>("createTableString"); + QTest::addColumn<QList<QDateTime> >("initialDateTimes"); + QTest::addColumn<QList<QDateTime> >("expectedDateTimes"); + + // Using time zones which are highly unlikely to be the same as the testing machine's one + // as it could pass as a result despite it. + // +8.5 hours from UTC to North Korea + const QTimeZone afterUTCTimeZone(30600); + // -8 hours from UTC to Belize + const QTimeZone beforeUTCTimeZone(-28800); + const QTimeZone utcTimeZone("UTC"); + const QDateTime dt(QDate(2015, 5, 18), QTime(4, 26, 30)); + const QDateTime dtWithMS(QDate(2015, 5, 18), QTime(4, 26, 30, 500)); + 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 QList<QDateTime> dateTimes = { dt, dtWithMS, dtWithAfterTZ, dtWithBeforeTZ, dtWithUTCTZ }; + const QList<QDateTime> expectedDateTimesLocalTZ = { dt, dtWithMS, dtWithAfterTZ.toLocalTime(), + dtWithBeforeTZ.toLocalTime(), + dtWithUTCTZ.toLocalTime() }; + const QList<QDateTime> expectedTimeStampDateTimes = { dt, dtWithMS, dtWithMS, dtWithMS, dtWithMS }; + const QList<QDateTime> expectedDateTimes = { dt, dt, dt, dt, dt }; + + for (const QString &dbName : qAsConst(dbs.dbNames)) { + QSqlDatabase db = QSqlDatabase::database(dbName); + if (!db.isValid()) + continue; + const QString tableNameTSWithTimeZone(qTableName("dateTimeTSWithTimeZone", __FILE__, db)); + const QString tableNameTSWithLocalTimeZone(qTableName("dateTimeTSWithLocalTimeZone", __FILE__, db)); + const QString tableNameTS(qTableName("dateTimeTS", __FILE__, db)); + const QString tableNameDate(qTableName("dateTimeDate", __FILE__, db)); + QTest::newRow(QString(dbName + " timestamp with time zone").toLatin1()) + << dbName << tableNameTSWithTimeZone + << QStringLiteral(" (dt TIMESTAMP WITH TIME ZONE)") + << dateTimes << dateTimes; + QTest::newRow(QString(dbName + " timestamp with local time zone").toLatin1()) + << dbName << tableNameTSWithTimeZone + << QStringLiteral(" (dt TIMESTAMP WITH LOCAL TIME ZONE)") + << dateTimes << expectedDateTimesLocalTZ; + QTest::newRow(QString(dbName + "timestamp").toLatin1()) + << dbName << tableNameTS << QStringLiteral(" (dt TIMESTAMP(3))") + << dateTimes << expectedTimeStampDateTimes; + QTest::newRow(QString(dbName + "date").toLatin1()) + << dbName << tableNameDate << QStringLiteral(" (dt DATE)") + << dateTimes << expectedDateTimes; + } +} + +void tst_QSqlQuery::dateTime() +{ + QFETCH(QString, dbName); + QSqlDatabase db = QSqlDatabase::database(dbName); + CHECK_DATABASE(db); + + QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db); + if (dbType != QSqlDriver::Oracle) + QSKIP("Implemented only for Oracle"); + + QFETCH(QString, tableName); + QFETCH(QString, createTableString); + QFETCH(QList<QDateTime>, initialDateTimes); + QFETCH(QList<QDateTime>, expectedDateTimes); + + tst_Databases::safeDropTable(db, tableName); + + QSqlQuery q(db); + QVERIFY_SQL(q, exec("CREATE TABLE " + tableName + createTableString)); + for (const QDateTime &dt : qAsConst(initialDateTimes)) { + QVERIFY_SQL(q, prepare("INSERT INTO " + tableName + " values(:dt)")); + q.bindValue(":dt", dt); + QVERIFY_SQL(q, exec()); + } + QVERIFY_SQL(q, exec("SELECT * FROM " + tableName)); + for (const QDateTime &dt : qAsConst(expectedDateTimes)) { + QVERIFY(q.next()); + QCOMPARE(q.value(0).toDateTime(), dt); + } +} + QTEST_MAIN( tst_QSqlQuery ) #include "tst_qsqlquery.moc" |