Adapt SQL drivers to Qt 6 change of QVariant::isNull
In Qt 5, QVariant::isNull returned true if either the variant didn't contain a value, or if the value was of a nullable type where the type's isNull member function returned true. In Qt 6, QVariant::isNull only returns true for variants that don't contain a value; if the value contained is e.g. a null-QString or QDateTime, then QVariant::isNull returns false. This change requires a follow up in the SQL drivers, which must still treat null-values the same as null-variants, lest they write data into the data base. Add a static helper to QSqlResultPrivate that implements isNull-checking of variants that contain a nullable type relevant for Sql, and add a test case to the QSqlQuery test that exercises that code. Pick-to: 6.2 6.3 Fixes: QTBUG-99408 Fixes: QTBUG-98471 Change-Id: I08b74a33aa3235c37d974f182da1f2bdcfd8217e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
parent
66f0149693
commit
999d856bc8
@ -697,7 +697,7 @@ bool QDB2Result::exec()
|
||||
for (i = 0; i < values.count(); ++i) {
|
||||
// bind parameters - only positional binding allowed
|
||||
SQLLEN *ind = &indicators[i];
|
||||
if (values.at(i).isNull())
|
||||
if (QSqlResultPrivate::isVariantNull(values.at(i)))
|
||||
*ind = SQL_NULL_DATA;
|
||||
if (bindValueType(i) & QSql::Out)
|
||||
values[i].detach();
|
||||
|
@ -968,7 +968,7 @@ bool QIBaseResult::exec()
|
||||
setAt(QSql::BeforeFirstRow);
|
||||
|
||||
if (d->inda) {
|
||||
QList<QVariant>& values = boundValues();
|
||||
const QList<QVariant> &values = boundValues();
|
||||
int i;
|
||||
if (values.count() > d->inda->sqld) {
|
||||
qWarning() << QLatin1String("QIBaseResult::exec: Parameter mismatch, expected") <<
|
||||
@ -984,7 +984,7 @@ bool QIBaseResult::exec()
|
||||
continue;
|
||||
const QVariant val(values[i]);
|
||||
if (d->inda->sqlvar[para].sqltype & 1) {
|
||||
if (val.isNull()) {
|
||||
if (QSqlResultPrivate::isVariantNull(val)) {
|
||||
// set null indicator
|
||||
*(d->inda->sqlvar[para].sqlind) = -1;
|
||||
// and set the value to 0, otherwise it would count as empty string.
|
||||
|
@ -929,7 +929,7 @@ bool QMYSQLResult::exec()
|
||||
|
||||
MYSQL_BIND* currBind = &d->outBinds[i];
|
||||
|
||||
nullVector[i] = static_cast<my_bool>(val.isNull());
|
||||
nullVector[i] = static_cast<my_bool>(QSqlResultPrivate::isVariantNull(val));
|
||||
currBind->is_null = &nullVector[i];
|
||||
currBind->length = 0;
|
||||
currBind->is_unsigned = 0;
|
||||
|
@ -515,7 +515,7 @@ int QOCIResultPrivate::bindValues(QVariantList &values, IndicatorArray &indicato
|
||||
|
||||
OCIBind * hbnd = nullptr; // Oracle handles these automatically
|
||||
sb2 *indPtr = &indicators[i];
|
||||
*indPtr = val.isNull() ? -1 : 0;
|
||||
*indPtr = QSqlResultPrivate::isVariantNull(val) ? -1 : 0;
|
||||
|
||||
bindValue(sql, &hbnd, err, i, val, indPtr, &tmpSizes[i], tmpStorage);
|
||||
}
|
||||
@ -1372,7 +1372,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a
|
||||
// not a list - create a deep-copy of the single value
|
||||
QOCIBatchColumn &singleCol = columns[i];
|
||||
singleCol.indicators = new sb2[1];
|
||||
*singleCol.indicators = boundValues.at(i).isNull() ? -1 : 0;
|
||||
*singleCol.indicators = QSqlResultPrivate::isVariantNull(boundValues.at(i)) ? -1 : 0;
|
||||
|
||||
r = d->bindValue(d->sql, &singleCol.bindh, d->err, i,
|
||||
boundValues.at(i), singleCol.indicators, &tmpSizes[i], tmpStorage);
|
||||
@ -1469,7 +1469,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a
|
||||
for (uint row = 0; row < col.recordCount; ++row) {
|
||||
const QVariant &val = boundValues.at(i).toList().at(row);
|
||||
|
||||
if (val.isNull() && !d->isOutValue(i)) {
|
||||
if (QSqlResultPrivate::isVariantNull(val) && !d->isOutValue(i)) {
|
||||
columns[i].indicators[row] = -1;
|
||||
columns[i].lengths[row] = 0;
|
||||
} else {
|
||||
|
@ -1419,7 +1419,7 @@ bool QODBCResult::exec()
|
||||
values[i].detach();
|
||||
const QVariant &val = values.at(i);
|
||||
SQLLEN *ind = &indicators[i];
|
||||
if (val.isNull())
|
||||
if (QSqlResultPrivate::isVariantNull(val))
|
||||
*ind = SQL_NULL_DATA;
|
||||
switch (val.typeId()) {
|
||||
case QMetaType::QDate: {
|
||||
|
@ -856,7 +856,7 @@ static QString qCreateParamString(const QList<QVariant> &boundValues, const QSql
|
||||
QSqlField f;
|
||||
for (const QVariant &val : boundValues) {
|
||||
f.setMetaType(val.metaType());
|
||||
if (val.isNull())
|
||||
if (QSqlResultPrivate::isVariantNull(val))
|
||||
f.clear();
|
||||
else
|
||||
f.setValue(val);
|
||||
|
@ -501,7 +501,7 @@ bool QSQLiteResult::exec()
|
||||
res = SQLITE_OK;
|
||||
const QVariant &value = values.at(i);
|
||||
|
||||
if (value.isNull()) {
|
||||
if (QSqlResultPrivate::isVariantNull(value)) {
|
||||
res = sqlite3_bind_null(d->stmt, i + 1);
|
||||
} else {
|
||||
switch (value.userType()) {
|
||||
|
@ -47,7 +47,9 @@
|
||||
#include "qsqlfield.h"
|
||||
#include "qsqlrecord.h"
|
||||
#include "qsqlresult_p.h"
|
||||
#include "quuid.h"
|
||||
#include "qvariant.h"
|
||||
#include "qdatetime.h"
|
||||
#include "private/qsqldriver_p.h"
|
||||
#include <QDebug>
|
||||
|
||||
@ -619,6 +621,31 @@ bool QSqlResult::prepare(const QString& query)
|
||||
return true; // fake prepares should always succeed
|
||||
}
|
||||
|
||||
bool QSqlResultPrivate::isVariantNull(const QVariant &variant)
|
||||
{
|
||||
if (variant.isNull())
|
||||
return true;
|
||||
|
||||
switch (variant.typeId()) {
|
||||
case qMetaTypeId<QString>():
|
||||
return static_cast<const QString*>(variant.constData())->isNull();
|
||||
case qMetaTypeId<QByteArray>():
|
||||
return static_cast<const QByteArray*>(variant.constData())->isNull();
|
||||
case qMetaTypeId<QDateTime>():
|
||||
return static_cast<const QDateTime*>(variant.constData())->isNull();
|
||||
case qMetaTypeId<QDate>():
|
||||
return static_cast<const QDate*>(variant.constData())->isNull();
|
||||
case qMetaTypeId<QTime>():
|
||||
return static_cast<const QTime*>(variant.constData())->isNull();
|
||||
case qMetaTypeId<QUuid>():
|
||||
return static_cast<const QUuid*>(variant.constData())->isNull();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
Executes the query, returning true if successful; otherwise returns
|
||||
false.
|
||||
@ -639,7 +666,10 @@ bool QSqlResult::exec()
|
||||
holder = d->holders.at(i).holderName;
|
||||
val = d->values.value(d->indexes.value(holder).value(0,-1));
|
||||
QSqlField f(QLatin1String(""), val.metaType());
|
||||
f.setValue(val);
|
||||
if (QSqlResultPrivate::isVariantNull(val))
|
||||
f.setValue(QVariant());
|
||||
else
|
||||
f.setValue(val);
|
||||
query = query.replace(d->holders.at(i).holderPos,
|
||||
holder.length(), driver()->formatValue(f));
|
||||
}
|
||||
@ -653,7 +683,7 @@ bool QSqlResult::exec()
|
||||
continue;
|
||||
QVariant var = d->values.value(idx);
|
||||
QSqlField f(QLatin1String(""), var.metaType());
|
||||
if (var.isNull())
|
||||
if (QSqlResultPrivate::isVariantNull(var))
|
||||
f.clear();
|
||||
else
|
||||
f.setValue(var);
|
||||
|
@ -133,6 +133,8 @@ public:
|
||||
bool active = false;
|
||||
bool isSel = false;
|
||||
bool forwardOnly = false;
|
||||
|
||||
static bool isVariantNull(const QVariant &variant);
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -64,6 +64,8 @@ private slots:
|
||||
void size();
|
||||
void isNull_data() { generic_data(); }
|
||||
void isNull();
|
||||
void writeNull_data() { generic_data(); }
|
||||
void writeNull();
|
||||
void query_exec_data() { generic_data(); }
|
||||
void query_exec();
|
||||
void execErrorRecovery_data() { generic_data(); }
|
||||
@ -355,6 +357,7 @@ void tst_QSqlQuery::dropTestTables( QSqlDatabase db )
|
||||
// drop all the table in case a testcase failed
|
||||
tablenames << qtest
|
||||
<< qTableName("qtest_null", __FILE__, db)
|
||||
<< qTableName("qtest_writenull", __FILE__, db)
|
||||
<< qTableName("qtest_blob", __FILE__, db)
|
||||
<< qTableName("qtest_bittest", __FILE__, db)
|
||||
<< qTableName("qtest_nullblob", __FILE__, db)
|
||||
@ -1769,6 +1772,95 @@ void tst_QSqlQuery::isNull()
|
||||
QVERIFY(q.isNull("unknown"));
|
||||
}
|
||||
|
||||
void tst_QSqlQuery::writeNull()
|
||||
{
|
||||
QFETCH(QString, dbName);
|
||||
QSqlDatabase db = QSqlDatabase::database(dbName);
|
||||
CHECK_DATABASE(db);
|
||||
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
|
||||
|
||||
QSqlQuery q(db);
|
||||
const QString tableName = qTableName("qtest_writenull", __FILE__, db);
|
||||
|
||||
// the test data table is already used, so use a local hash to exercise the various
|
||||
// cases from the QSqlResultPrivate::isVariantNull helper. Only PostgreSQL supports
|
||||
// QUuid.
|
||||
QMultiHash<QString, QVariant> nullableTypes = {
|
||||
{"varchar(20)", QString("not null")},
|
||||
{"varchar(20)", QByteArray("not null")},
|
||||
{"date", QDateTime::currentDateTime()},
|
||||
{"date", QDate::currentDate()},
|
||||
{"date", QTime::currentTime()},
|
||||
};
|
||||
if (dbType == QSqlDriver::PostgreSQL)
|
||||
nullableTypes["uuid"] = QUuid::createUuid();
|
||||
|
||||
// Helper to count rows with null values in the data column.
|
||||
// Since QSqlDriver::QuerySize might not be supported, we have to count anyway
|
||||
const auto countRowsWithNull = [&]{
|
||||
q.exec("select id, data from " + tableName + " where data is null");
|
||||
int size = 0;
|
||||
while (q.next())
|
||||
++size;
|
||||
return size;
|
||||
};
|
||||
|
||||
for (const auto &nullableType : nullableTypes.keys()) {
|
||||
auto tableGuard = qScopeGuard([&]{
|
||||
q.exec("drop table " + tableName);
|
||||
});
|
||||
const QVariant nonNullValue = nullableTypes.value(nullableType);
|
||||
// some useful diagnostic output in case of any test failure
|
||||
auto errorHandler = qScopeGuard([&]{
|
||||
qWarning() << "Test failure for data type" << nonNullValue.metaType().name();
|
||||
q.exec("select id, data from " + tableName);
|
||||
while (q.next())
|
||||
qWarning() << q.value(0) << q.value(1);
|
||||
});
|
||||
QString createQuery = "create table " + tableName + " (id int, data " + nullableType;
|
||||
if (dbType == QSqlDriver::MSSqlServer || dbType == QSqlDriver::Sybase)
|
||||
createQuery += " null";
|
||||
createQuery += ")";
|
||||
QVERIFY_SQL(q, exec(createQuery));
|
||||
|
||||
int expectedNullCount = 0;
|
||||
// verify that inserting a non-null value works
|
||||
QVERIFY_SQL(q, prepare("insert into " + tableName + " values(:id, :data)"));
|
||||
q.bindValue(":id", expectedNullCount);
|
||||
q.bindValue(":data", nonNullValue);
|
||||
QVERIFY_SQL(q, exec());
|
||||
QCOMPARE(countRowsWithNull(), expectedNullCount);
|
||||
|
||||
// verify that inserting using a null QVariant produces a null entry in the database
|
||||
QVERIFY_SQL(q, prepare("insert into " + tableName + " values(:id, :data)"));
|
||||
q.bindValue(":id", ++expectedNullCount);
|
||||
q.bindValue(":data", QVariant());
|
||||
QVERIFY_SQL(q, exec());
|
||||
QCOMPARE(countRowsWithNull(), expectedNullCount);
|
||||
|
||||
// verify that writing a null-value (but not a null-variant) produces a null entry in the database
|
||||
const QMetaType nullableMetaType = nullableTypes.value(nullableType).metaType();
|
||||
// creating a QVariant with meta type and nullptr does create a null-QVariant. We want
|
||||
// to explicitly create a non-null variant, so we have to pass in a default-constructed
|
||||
// value as well (and make sure that the default value is also destroyed again,
|
||||
// which is clumsy to do using std::unique_ptr with a custom deleter, so use another
|
||||
// scope guard).
|
||||
void* defaultData = nullableMetaType.create();
|
||||
const auto defaultTypeDeleter = qScopeGuard([&]{ nullableMetaType.destroy(defaultData); });
|
||||
const QVariant nullValueVariant(nullableMetaType, defaultData);
|
||||
QVERIFY(!nullValueVariant.isNull());
|
||||
|
||||
QVERIFY_SQL(q, prepare("insert into " + tableName + " values(:id, :data)"));
|
||||
q.bindValue(":id", ++expectedNullCount);
|
||||
q.bindValue(":data", nullValueVariant);
|
||||
QVERIFY_SQL(q, exec());
|
||||
QCOMPARE(countRowsWithNull(), expectedNullCount);
|
||||
|
||||
// all tests passed for this type if we got here, so don't print diagnostics
|
||||
errorHandler.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/*! TDS specific BIT field test */
|
||||
void tst_QSqlQuery::tds_bitField()
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user