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 #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) QSQLiteDriver::QSQLiteDriver(QObject * parent)
: QSqlDriver(*new QSQLiteDriverPrivate, parent) : QSqlDriver(*new QSQLiteDriverPrivate, parent)
{ {
@ -692,6 +716,7 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
bool openUriOption = false; bool openUriOption = false;
bool useExtendedResultCodes = true; bool useExtendedResultCodes = true;
bool useQtVfs = false; bool useQtVfs = false;
bool useQtCaseFolding = false;
#if QT_CONFIG(regularexpression) #if QT_CONFIG(regularexpression)
static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1; static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1;
bool defineRegexp = false; bool defineRegexp = false;
@ -719,6 +744,8 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
sharedCache = true; sharedCache = true;
} else if (option == "QSQLITE_NO_USE_EXTENDED_RESULT_CODES"_L1) { } else if (option == "QSQLITE_NO_USE_EXTENDED_RESULT_CODES"_L1) {
useExtendedResultCodes = false; useExtendedResultCodes = false;
} else if (option == "QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING"_L1) {
useQtCaseFolding = true;
} }
#if QT_CONFIG(regularexpression) #if QT_CONFIG(regularexpression)
else if (option.startsWith(regexpConnectOption)) { else if (option.startsWith(regexpConnectOption)) {
@ -760,6 +787,12 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
nullptr, &_q_regexp_cleanup); nullptr, &_q_regexp_cleanup);
} }
#endif #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; return true;
} else { } else {
setLastError(qMakeError(d->access, tr("Error opening database"), setLastError(qMakeError(d->access, tr("Error opening database"),

View File

@ -757,6 +757,10 @@
\li QSQLITE_NO_USE_EXTENDED_RESULT_CODES \li QSQLITE_NO_USE_EXTENDED_RESULT_CODES
\li Disables the usage of the \l {https://www.sqlite.org/c3ref/extended_result_codes.html} \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) {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 \endtable
\section3 How to Build the QSQLITE Plugin \section3 How to Build the QSQLITE Plugin

View File

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

View File

@ -4454,7 +4454,7 @@ void tst_QSqlQuery::aggregateFunctionTypes()
.arg(tableName))); .arg(tableName)));
QVERIFY_SQL(q, exec("SELECT MAX(txt) FROM " + tableName)); QVERIFY_SQL(q, exec("SELECT MAX(txt) FROM " + tableName));
QVERIFY(q.next()); QVERIFY_SQL(q, next());
if (dbType == QSqlDriver::SQLite) if (dbType == QSqlDriver::SQLite)
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::UnknownType); QCOMPARE(q.record().field(0).metaType().id(), QMetaType::UnknownType);
else else
@ -4469,6 +4469,19 @@ void tst_QSqlQuery::aggregateFunctionTypes()
QVERIFY(q.next()); QVERIFY(q.next());
QCOMPARE(q.value(0).toString(), QLatin1String("upper")); QCOMPARE(q.value(0).toString(), QLatin1String("upper"));
QCOMPARE(q.record().field(0).metaType().id(), QMetaType::QString); 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);
} }
} }