REGEXP for SQLite

Since SQLite does not define a regexp function by default, provide a Qt
based implementation which can be enabled using QSQLITE_ENABLE_REGEXP as
an connect option. This way statements like

SELECT * FROM table WHERE col REGEXP '^[a-d]';

work out of the box.

[ChangeLog][QtSql] Add QSQLITE_ENABLE_REGEXP connect option for
QSQLiteDriver. If set a Qt based regexp() implementation is provided
allowing to use REGEXP in SQL statements.

Task-number: QTBUG-18084
Change-Id: I7f0e926fe4c5d6baea509f75497f46a61ca86679
Reviewed-by: Milian Wolff <milian.wolff@kdab.com>
Reviewed-by: Jesus Fernandez <Jesus.Fernandez@qt.io>
Reviewed-by: Sebastian Sauer <sebastian.sauer@kdab.com>
Reviewed-by: Andy Shaw <andy.shaw@qt.io>
Reviewed-by: Frederik Gladhorn <frederik.gladhorn@qt.io>
This commit is contained in:
Lorenz Haas 2016-11-19 19:05:36 +01:00
parent 56723c6e91
commit 2a3297c726
5 changed files with 116 additions and 0 deletions

View File

@ -51,6 +51,10 @@
#include <qstringlist.h> #include <qstringlist.h>
#include <qvector.h> #include <qvector.h>
#include <qdebug.h> #include <qdebug.h>
#ifndef QT_NO_REGULAREXPRESSION
#include <qcache.h>
#include <qregularexpression.h>
#endif
#if defined Q_OS_WIN #if defined Q_OS_WIN
# include <qt_windows.h> # include <qt_windows.h>
@ -556,6 +560,37 @@ QVariant QSQLiteResult::handle() const
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
#ifndef QT_NO_REGULAREXPRESSION
static void _q_regexp(sqlite3_context* context, int argc, sqlite3_value** argv)
{
if (Q_UNLIKELY(argc != 2)) {
sqlite3_result_int(context, 0);
return;
}
const QString pattern = QString::fromUtf8(
reinterpret_cast<const char*>(sqlite3_value_text(argv[0])));
const QString subject = QString::fromUtf8(
reinterpret_cast<const char*>(sqlite3_value_text(argv[1])));
auto cache = static_cast<QCache<QString, QRegularExpression>*>(sqlite3_user_data(context));
QRegularExpression *regexp = cache->object(pattern);
if (!regexp) {
regexp = new QRegularExpression(pattern, QRegularExpression::DontCaptureOption
| QRegularExpression::OptimizeOnFirstUsageOption);
cache->insert(pattern, regexp);
}
const bool found = subject.contains(*regexp);
sqlite3_result_int(context, int(found));
}
static void _q_regexp_cleanup(void *cache)
{
delete static_cast<QCache<QString, QRegularExpression>*>(cache);
}
#endif
QSQLiteDriver::QSQLiteDriver(QObject * parent) QSQLiteDriver::QSQLiteDriver(QObject * parent)
: QSqlDriver(*new QSQLiteDriverPrivate, parent) : QSqlDriver(*new QSQLiteDriverPrivate, parent)
{ {
@ -615,6 +650,11 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
bool sharedCache = false; bool sharedCache = false;
bool openReadOnlyOption = false; bool openReadOnlyOption = false;
bool openUriOption = false; bool openUriOption = false;
#ifndef QT_NO_REGULAREXPRESSION
static const QLatin1String regexpConnectOption = QLatin1String("QSQLITE_ENABLE_REGEXP");
bool defineRegexp = false;
int regexpCacheSize = 25;
#endif
const auto opts = conOpts.splitRef(QLatin1Char(';')); const auto opts = conOpts.splitRef(QLatin1Char(';'));
for (auto option : opts) { for (auto option : opts) {
@ -634,6 +674,22 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
} else if (option == QLatin1String("QSQLITE_ENABLE_SHARED_CACHE")) { } else if (option == QLatin1String("QSQLITE_ENABLE_SHARED_CACHE")) {
sharedCache = true; sharedCache = true;
} }
#ifndef QT_NO_REGULAREXPRESSION
else if (option.startsWith(regexpConnectOption)) {
option = option.mid(regexpConnectOption.size()).trimmed();
if (option.isEmpty()) {
defineRegexp = true;
} else if (option.startsWith(QLatin1Char('='))) {
bool ok = false;
const int cacheSize = option.mid(1).trimmed().toInt(&ok);
if (ok) {
defineRegexp = true;
if (cacheSize > 0)
regexpCacheSize = cacheSize;
}
}
}
#endif
} }
int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE));
@ -646,6 +702,13 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c
sqlite3_busy_timeout(d->access, timeOut); sqlite3_busy_timeout(d->access, timeOut);
setOpen(true); setOpen(true);
setOpenError(false); setOpenError(false);
#ifndef QT_NO_REGULAREXPRESSION
if (defineRegexp) {
auto cache = new QCache<QString, QRegularExpression>(regexpCacheSize);
sqlite3_create_function_v2(d->access, "regexp", 2, SQLITE_UTF8, cache, &_q_regexp, NULL,
NULL, &_q_regexp_cleanup);
}
#endif
return true; return true;
} else { } else {
if (d->access) { if (d->access) {

View File

@ -90,3 +90,8 @@ qDebug() << q.value(0); // outputs the first RETURN/OUT value
QSqlDatabase: QMYSQL driver not loaded QSqlDatabase: QMYSQL driver not loaded
QSqlDatabase: available drivers: QMYSQL QSqlDatabase: available drivers: QMYSQL
//! [31] //! [31]
//! [34]
column.contains(QRegularExpression("pattern"));
//! [34]

View File

@ -568,6 +568,22 @@
\snippet code/doc_src_sql-driver.qdoc 23 \snippet code/doc_src_sql-driver.qdoc 23
\section3 Enable REGEXP operator
SQLite comes with a REGEXP operation. However the needed implementation must
be provided by the user. For convenience a default implementation can be
enabled by \l{QSqlDatabase::setConnectOptions()} {setting the connect
option} \c{QSQLITE_ENABLE_REGEXP} before \l{QSqlDatabase::open()} {the
database connection is opened}. Then a SQL statement like "column REGEXP
'pattern'" basically expands to the Qt code
\snippet code/doc_src_sql-driver.cpp 34
For better performance the regular expressions are cached internally. By
default the cache size is 25, but it can be changed through the option's
value. For example passing "\c{QSQLITE_ENABLE_REGEXP=10}" reduces the
cache size to 10.
\section3 QSQLITE File Format Compatibility \section3 QSQLITE File Format Compatibility
SQLite minor releases sometimes break file format forward compatibility. SQLite minor releases sometimes break file format forward compatibility.

View File

@ -1175,6 +1175,7 @@ QSqlRecord QSqlDatabase::record(const QString& tablename) const
\li QSQLITE_OPEN_READONLY \li QSQLITE_OPEN_READONLY
\li QSQLITE_OPEN_URI \li QSQLITE_OPEN_URI
\li QSQLITE_ENABLE_SHARED_CACHE \li QSQLITE_ENABLE_SHARED_CACHE
\li QSQLITE_ENABLE_REGEXP
\endlist \endlist
\li \li

View File

@ -188,6 +188,9 @@ private slots:
void sqlite_enable_cache_mode_data() { generic_data("QSQLITE"); } void sqlite_enable_cache_mode_data() { generic_data("QSQLITE"); }
void sqlite_enable_cache_mode(); void sqlite_enable_cache_mode();
void sqlite_enableRegexp_data() { generic_data("QSQLITE"); }
void sqlite_enableRegexp();
private: private:
void createTestTables(QSqlDatabase db); void createTestTables(QSqlDatabase db);
void dropTestTables(QSqlDatabase db); void dropTestTables(QSqlDatabase db);
@ -2259,5 +2262,33 @@ void tst_QSqlDatabase::sqlite_enable_cache_mode()
db2.close(); db2.close();
} }
void tst_QSqlDatabase::sqlite_enableRegexp()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
if (db.driverName().startsWith("QSQLITE2"))
QSKIP("SQLite3 specific test");
db.close();
db.setConnectOptions("QSQLITE_ENABLE_REGEXP");
QVERIFY_SQL(db, open());
QSqlQuery q(db);
const QString tableName(qTableName("uint_test", __FILE__, db));
QVERIFY_SQL(q, exec(QString("CREATE TABLE %1(text TEXT)").arg(tableName)));
QVERIFY_SQL(q, prepare(QString("INSERT INTO %1 VALUES(?)").arg(tableName)));
q.addBindValue("a0");
QVERIFY_SQL(q, exec());
q.addBindValue("a1");
QVERIFY_SQL(q, exec());
QVERIFY_SQL(q, exec(QString("SELECT text FROM %1 WHERE text REGEXP 'a[^0]' "
"ORDER BY text").arg(tableName)));
QVERIFY_SQL(q, next());
QCOMPARE(q.value(0).toString(), QString("a1"));
QFAIL_SQL(q, next());
}
QTEST_MAIN(tst_QSqlDatabase) QTEST_MAIN(tst_QSqlDatabase)
#include "tst_qsqldatabase.moc" #include "tst_qsqldatabase.moc"