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:
Volker Hilsheimer 2021-12-22 15:09:23 +01:00
parent 66f0149693
commit 999d856bc8
10 changed files with 136 additions and 12 deletions

View File

@ -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();

View File

@ -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.

View File

@ -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;

View File

@ -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 {

View File

@ -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: {

View File

@ -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);

View File

@ -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()) {

View File

@ -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);

View File

@ -133,6 +133,8 @@ public:
bool active = false;
bool isSel = false;
bool forwardOnly = false;
static bool isVariantNull(const QVariant &variant);
};
QT_END_NAMESPACE

View File

@ -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()
{