SQL/IBase: add partial support for SQL_INT128 datatype

Implement partial support for SQL_INT128 datatype which is used for
DECIMAL/NUMERIC columns with a precision > 18. This support is only
available when QT_SUPPORTS_INT128 is defined and for MSVC.
Binding values to columns which need SQL_INT128 is supported but
numbers given as QString will be converted to doubles even if
QSql::HighPrecision is set.

[ChangeLog][SQL][IBASE] Added support for SQL_INT128 datatype.

Task-number: QTBUG-124575
Change-Id: If3fb1eb0f19dc60f000d163f3bf45da7acb08c87
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
Reviewed-by: Andreas Bacher <andi.bacher@outlook.com>
This commit is contained in:
Christian Ehrlicher 2024-04-20 20:51:50 +02:00
parent e9c6adb992
commit 373ae6cbd2
2 changed files with 149 additions and 46 deletions

View File

@ -3,11 +3,13 @@
#include "qsql_ibase_p.h"
#include <QtCore/qcoreapplication.h>
#include <QtCore/qendian.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qtimezone.h>
#include <QtCore/qdeadlinetimer.h>
#include <QtCore/qdebug.h>
#include <QtCore/qlist.h>
#include <QtCore/private/qlocale_tools_p.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmap.h>
#include <QtCore/qmutex.h>
@ -41,6 +43,10 @@ using namespace Qt::StringLiterals;
#define blr_boolean_dtype blr_bool
#endif
#if (defined(QT_SUPPORTS_INT128) || defined(Q_CC_MSVC)) && (FB_API_VER >= 40)
#define IBASE_INT128_SUPPORTED
#endif
constexpr qsizetype QIBaseChunkSize = SHRT_MAX / 2;
#if (FB_API_VER >= 40)
@ -100,6 +106,9 @@ static void initDA(XSQLDA *sqlda)
case SQL_TIMESTAMP:
#if (FB_API_VER >= 40)
case SQL_TIMESTAMP_TZ:
#ifdef IBASE_INT128_SUPPORTED
case SQL_INT128:
#endif
#endif
case SQL_TYPE_TIME:
case SQL_TYPE_DATE:
@ -400,6 +409,32 @@ protected:
int numRowsAffected() override;
QSqlRecord record() const override;
template<typename T>
static QString numberToHighPrecision(T val, int scale)
{
const bool negative = val < 0;
QString number;
#ifdef IBASE_INT128_SUPPORTED
if constexpr (std::is_same_v<qinternalint128, T>) {
number = negative ? qint128toBasicLatin(val * -1)
: qint128toBasicLatin(val);
} else
#endif
number = negative ? QString::number(qAbs(val))
: QString::number(val);
auto len = number.size();
scale *= -1;
if (scale >= len) {
number = QString(scale - len + 1, u'0') + number;
len = number.size();
}
const auto sepPos = len - scale;
number = number.left(sepPos) + u'.' + number.mid(sepPos);
if (negative)
number = u'-' + number;
return number;
}
template<typename T>
QVariant applyScale(T val, int scale) const
{
@ -413,25 +448,8 @@ protected:
return QVariant(qint64(val * pow(10.0, scale)));
case QSql::LowPrecisionDouble:
return QVariant(double(val * pow(10.0, scale)));
case QSql::HighPrecision: {
const bool negative = val < 0;
QString number;
if constexpr (std::is_signed_v<T> || negative)
number = QString::number(qAbs(val));
else
number = QString::number(val);
auto len = number.size();
scale *= -1;
if (scale >= len) {
number = QString(scale - len + 1, u'0') + number;
len = number.size();
}
const auto sepPos = len - scale;
number = number.left(sepPos) + u'.' + number.mid(sepPos);
if (negative)
number = u'-' + number;
return QVariant(number);
}
case QSql::HighPrecision:
return QVariant(numberToHighPrecision(val, scale));
}
return QVariant(val);
}
@ -440,8 +458,23 @@ protected:
void setWithScale(const QVariant &val, int scale, char *data)
{
auto ptr = reinterpret_cast<T *>(data);
if (scale < 0)
*ptr = static_cast<T>(floor(0.5 + val.toDouble() * pow(10.0, scale * -1)));
if (scale < 0) {
double d = floor(0.5 + val.toDouble() * pow(10.0, scale * -1));
#ifdef IBASE_INT128_SUPPORTED
if constexpr (std::is_same_v<qinternalint128, T>) {
quint64 lower = quint64(d);
quint64 tmp = quint64(std::numeric_limits<quint32>::max()) + 1;
d /= tmp;
d /= tmp;
quint64 higher = quint64(d);
qinternalint128 result = higher;
result <<= 64;
result += lower;
*ptr = static_cast<T>(result);
} else
#endif
*ptr = static_cast<T>(d);
}
else
*ptr = val.value<T>();
}
@ -1107,6 +1140,12 @@ bool QIBaseResult::exec()
case SQL_INT64:
setWithScale<qint64>(val, d->inda->sqlvar[para].sqlscale, d->inda->sqlvar[para].sqldata);
break;
#ifdef IBASE_INT128_SUPPORTED
case SQL_INT128:
setWithScale<qinternalint128>(val, d->inda->sqlvar[para].sqlscale,
d->inda->sqlvar[para].sqldata);
break;
#endif
case SQL_LONG:
if (d->inda->sqlvar[para].sqllen == 4)
setWithScale<qint32>(val, d->inda->sqlvar[para].sqlscale, d->inda->sqlvar[para].sqldata);
@ -1272,6 +1311,17 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx)
row[idx] = applyScale(val, scale);
break;
}
#ifdef IBASE_INT128_SUPPORTED
case SQL_INT128: {
Q_ASSERT(d->sqlda->sqlvar[i].sqllen == sizeof(qinternalint128));
const qinternalint128 val128 = qFromUnaligned<qinternalint128>(buf);
const auto scale = d->sqlda->sqlvar[i].sqlscale;
row[idx] = numberToHighPrecision(val128, scale);
if (numericalPrecisionPolicy() != QSql::HighPrecision)
row[idx] = applyScale(row[idx].toDouble(), 0);
break;
}
#endif
case SQL_LONG:
if (d->sqlda->sqlvar[i].sqllen == 4) {
const auto val = *(qint32 *)buf;

View File

@ -256,6 +256,9 @@ private slots:
void ibaseDateTimeWithTZ();
void ibaseTimeStampTzArray_data() { generic_data("QIBASE"); }
void ibaseTimeStampTzArray();
void ibaseInt128_data() { generic_data("QIBASE"); }
void ibaseInt128();
void psqlJsonOperator_data() { generic_data("QPSQL"); }
void psqlJsonOperator();
@ -1872,6 +1875,9 @@ void tst_QSqlQuery::oci_rawField()
QCOMPARE(q.value(0).toByteArray().size(), qsizetype(7));
}
// Test whether we can fetch values with more than DOUBLE precision
// note that SQLite highest precision is that of a double, although
// you can define field with higher precision:
// Test whether we can fetch values with more than DOUBLE precision
// note that SQLite highest precision is that of a double, although
// you can define field with higher precision:
@ -1881,46 +1887,45 @@ void tst_QSqlQuery::precision()
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
const QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
if (dbType == QSqlDriver::Interbase)
QSKIP("DB unable to store high precision");
const auto tidier = qScopeGuard([db, oldPrecision = db.driver()->numericalPrecisionPolicy()]() {
db.driver()->setNumericalPrecisionPolicy(oldPrecision);
});
int digits = 21;
int decimals = 20;
std::array<QLatin1String, 2> precStrings = { "1.2345678901234567891"_L1,
"-1.2345678901234567891"_L1 };
if (dbType == QSqlDriver::SQLite) {
// SQLite 3.45 does not return more, even when speicfied
digits = 17;
decimals = 16;
precStrings = { "1.2345678901234567"_L1, "-1.2345678901234567"_L1 };
} else if (dbType == QSqlDriver::Sybase)
decimals = 18;
db.driver()->setNumericalPrecisionPolicy(QSql::HighPrecision);
TableScope ts(db, "qtest_precision", __FILE__);
static const QLatin1String precStr("1.2345678901234567891");
{
// need a new scope for SQLITE
QSqlQuery q(db);
QVERIFY_SQL(q, exec(QLatin1String(tst_Databases::isMSAccess(db)
? "CREATE TABLE %1 (col1 number)"
: "CREATE TABLE %1 (col1 numeric(21, 20))")
.arg(ts.tableName())));
QVERIFY_SQL(q, exec(QLatin1String("INSERT INTO %1 (col1) VALUES (%2)")
.arg(ts.tableName(), precStr)));
QSqlQuery q(db);
QString stmt = "CREATE TABLE %1 (col1 numeric("_L1 + QString::number(digits) + ", "_L1 +
QString::number(decimals) + "))"_L1;
if (tst_Databases::isMSAccess(db))
stmt = "CREATE TABLE %1 (col1 number)"_L1;
QVERIFY_SQL(q, exec(stmt.arg(ts.tableName())));
for (const auto &precStr : precStrings) {
QVERIFY_SQL(q, exec("DELETE FROM %1"_L1.arg(ts.tableName())));
QVERIFY_SQL(q, exec("INSERT INTO %1 (col1) VALUES (%2)"_L1.arg(ts.tableName(), precStr)));
QVERIFY_SQL(q, exec("SELECT * FROM " + ts.tableName()));
QVERIFY(q.next());
const QString val = q.value(0).toString();
if (!val.startsWith(precStr)) {
int i = 0;
while (i < val.size() && precStr[i] != 0 && precStr[i] == val[i].toLatin1())
while (i < val.size() && precStr[i] != 0 && precStr[i] == val[i])
++i;
// TDS has crappy precisions by default
if (dbType == QSqlDriver::Sybase) {
if (i < 18)
qWarning("TDS didn't return the right precision");
} else {
qWarning() << tst_Databases::dbToString(db) << "didn't return the right precision ("
<< i << "out of 21)," << val;
}
qWarning() << tst_Databases::dbToString(db) << "didn't return the right precision ("
<< i << "out of " << digits << ")," << val;
}
} // SQLITE scope
}
}
void tst_QSqlQuery::nullResult()
@ -5034,6 +5039,54 @@ void tst_QSqlQuery::ibaseTimeStampTzArray()
#endif // QT_CONFIG(timezone)
}
void tst_QSqlQuery::ibaseInt128()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
TableScope ts(db, "int128test", __FILE__);
db.setNumericalPrecisionPolicy(QSql::HighPrecision);
QSqlQuery q(db);
if (!q.exec("CREATE TABLE " + ts.tableName() + " (id INT PRIMARY KEY, price NUMERIC(20, 4))"))
QSKIP("Need at least Firebird 4 for this test - skipping");
QVERIFY_SQL(q, exec("INSERT INTO " + ts.tableName() + "(id,price) values(1,40001.1234)"));
QVERIFY_SQL(q, prepare("INSERT INTO " + ts.tableName() + "(id,price) values(2,:amount)"));
q.bindValue(":amount", 12345.67890);
QVERIFY_SQL(q, exec());
{
QSqlQuery q2(db);
q2.setNumericalPrecisionPolicy(QSql::LowPrecisionDouble);
QVERIFY_SQL(q2, exec("SELECT price FROM " + ts.tableName() + " ORDER BY id"));
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::Double);
QCOMPARE(q2.value(0).toDouble(), 40001.1234);
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::Double);
QCOMPARE(q2.value(0).toDouble(), 12345.6789);
QVERIFY_SQL(q2, exec("SELECT sum(price) FROM " + ts.tableName()));
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::Double);
QCOMPARE(q2.value(0).toDouble(), 52346.8023);
}
{
QSqlQuery q2(db);
q2.setNumericalPrecisionPolicy(QSql::HighPrecision);
QVERIFY_SQL(q2, exec("SELECT price FROM " + ts.tableName() + " ORDER BY id"));
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::QString);
QCOMPARE(q2.value(0).toString(), "40001.1234");
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::QString);
QCOMPARE(q2.value(0).toString(), "12345.6789");
QVERIFY_SQL(q2, exec("SELECT sum(price) FROM " + ts.tableName()));
QVERIFY_SQL(q2, next());
QCOMPARE(q2.value(0).metaType().id(), QMetaType::QString);
QCOMPARE(q2.value(0).toString(), "52346.8023");
}
}
void tst_QSqlQuery::ibase_executeBlock()
{
QFETCH(QString, dbName);