SQL/SQLite: add case folding for non-ascii characters

SQLite does not provide a proper case folding for non-ascii characters
due to a lack of a proper ICU library. Therefore add an option so Qt can
do it for SQLite.
[ChangeLog][SQL][SQLite] Add new option
QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING for correct case folding of
non-ascii characters.

Fixes: QTBUG-18871
Change-Id: Ib62fedf750f05e50a581604253cf30d81e367b42
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Christian Ehrlicher 2023-03-27 21:20:43 +02:00
parent 98e4e992fe
commit 4b7b5edf26
4 changed files with 54 additions and 2 deletions

View File

@ -625,6 +625,30 @@ static void _q_regexp_cleanup(void *cache)
}
#endif
static void _q_lower(sqlite3_context* context, int argc, sqlite3_value** argv)
{
if (Q_UNLIKELY(argc != 1)) {
sqlite3_result_text(context, nullptr, 0, nullptr);
return;
}
const QString lower = QString::fromUtf8(
reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toLower();
const QByteArray ba = lower.toUtf8();
sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT);
}
static void _q_upper(sqlite3_context* context, int argc, sqlite3_value** argv)
{
if (Q_UNLIKELY(argc != 1)) {
sqlite3_result_text(context, nullptr, 0, nullptr);
return;
}
const QString upper = QString::fromUtf8(
reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toUpper();
const QByteArray ba = upper.toUtf8();
sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT);
}
QSQLiteDriver::QSQLiteDriver(QObject * parent)
: QSqlDriver(*new QSQLiteDriverPrivate, parent)
{
@ -692,6 +716,7 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
bool openUriOption = false;
bool useExtendedResultCodes = true;
bool useQtVfs = false;
bool useQtCaseFolding = false;
#if QT_CONFIG(regularexpression)
static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1;
bool defineRegexp = false;
@ -719,6 +744,8 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
sharedCache = true;
} else if (option == "QSQLITE_NO_USE_EXTENDED_RESULT_CODES"_L1) {
useExtendedResultCodes = false;
} else if (option == "QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING"_L1) {
useQtCaseFolding = true;
}
#if QT_CONFIG(regularexpression)
else if (option.startsWith(regexpConnectOption)) {
@ -760,6 +787,12 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
nullptr, &_q_regexp_cleanup);
}
#endif
if (useQtCaseFolding) {
sqlite3_create_function_v2(d->access, "lower", 1, SQLITE_UTF8, nullptr,
&_q_lower, nullptr, nullptr, nullptr);
sqlite3_create_function_v2(d->access, "upper", 1, SQLITE_UTF8, nullptr,
&_q_upper, nullptr, nullptr, nullptr);
}
return true;
} else {
setLastError(qMakeError(d->access, tr("Error opening database"),

View File

@ -757,6 +757,10 @@
\li QSQLITE_NO_USE_EXTENDED_RESULT_CODES
\li Disables the usage of the \l {https://www.sqlite.org/c3ref/extended_result_codes.html}
{extended result code} feature in SQLite (for backwards compatibility)
\row
\li QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING
\li If set, the plugin replaces the functions 'lower' and 'upper' with
QString functions for correct case folding of non-ascii characters
\endtable
\section3 How to Build the QSQLITE Plugin

View File

@ -118,12 +118,14 @@ public:
if (port > 0)
cName += QLatin1Char(':') + QString::number(port);
QString opts = params;
if (driver == "QSQLITE") {
// Since the database for sqlite is generated at runtime it's always
// available, but we use QTempDir so it's always in a different
// location. Thus, let's ignore the path completely.
cName = "SQLite";
qInfo("SQLite will use the database located at %ls", qUtf16Printable(dbName));
opts += QStringLiteral(";QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING");
}
auto db = QSqlDatabase::addDatabase(driver, cName);
@ -137,7 +139,7 @@ public:
db.setPassword(passwd);
db.setHostName(host);
db.setPort(port);
db.setConnectOptions(params);
db.setConnectOptions(opts);
dbNames.append(cName);
}

View File

@ -4454,7 +4454,7 @@ void tst_QSqlQuery::aggregateFunctionTypes()
.arg(tableName)));
QVERIFY_SQL(q, exec("SELECT MAX(txt) FROM " + tableName));
QVERIFY(q.next());
QVERIFY_SQL(q, next());
if (dbType == QSqlDriver::SQLite)
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::UnknownType);
else
@ -4469,6 +4469,19 @@ void tst_QSqlQuery::aggregateFunctionTypes()
QVERIFY(q.next());
QCOMPARE(q.value(0).toString(), QLatin1String("upper"));
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::QString);
QVERIFY_SQL(q, exec(QLatin1String("DELETE FROM %1").arg(tableName)));
QVERIFY_SQL(q, exec(QString::fromUtf8("INSERT INTO %1 (id, txt) VALUES (1, 'löW€RÄ')")
.arg(tableName)));
QVERIFY_SQL(q, exec("SELECT LOWER(txt) FROM " + tableName));
QVERIFY(q.next());
QCOMPARE(q.value(0).toString(), QString::fromUtf8("löw€rä"));
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::QString);
QVERIFY_SQL(q, exec("SELECT UPPER(txt) FROM " + tableName));
QVERIFY(q.next());
QCOMPARE(q.value(0).toString(), QString::fromUtf8("LÖW€RÄ"));
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::QString);
}
}