diff options
-rw-r--r-- | src/plugins/sqldrivers/sqlite/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp | 5 | ||||
-rw-r--r-- | src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp | 255 | ||||
-rw-r--r-- | src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h | 21 | ||||
-rw-r--r-- | src/plugins/sqldrivers/sqlite/smain.cpp | 2 | ||||
-rw-r--r-- | src/sql/doc/src/sql-driver.qdoc | 11 | ||||
-rw-r--r-- | tests/auto/sql/kernel/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/sql/kernel/qvfssql/CMakeLists.txt | 24 | ||||
-rw-r--r-- | tests/auto/sql/kernel/qvfssql/sample.db | bin | 0 -> 49152 bytes | |||
-rw-r--r-- | tests/auto/sql/kernel/qvfssql/tst_qvfssql.cpp | 94 |
10 files changed, 413 insertions, 1 deletions
diff --git a/src/plugins/sqldrivers/sqlite/CMakeLists.txt b/src/plugins/sqldrivers/sqlite/CMakeLists.txt index add9dff9fd..6b31b1018c 100644 --- a/src/plugins/sqldrivers/sqlite/CMakeLists.txt +++ b/src/plugins/sqldrivers/sqlite/CMakeLists.txt @@ -10,6 +10,7 @@ qt_internal_add_plugin(QSQLiteDriverPlugin PLUGIN_TYPE sqldrivers SOURCES qsql_sqlite.cpp qsql_sqlite_p.h + qsql_sqlite_vfs.cpp qsql_sqlite_vfs_p.h smain.cpp DEFINES QT_NO_CAST_FROM_ASCII diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp index 60661e6824..c5d1e7a2c7 100644 --- a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp @@ -691,6 +691,7 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c bool openReadOnlyOption = false; bool openUriOption = false; bool useExtendedResultCodes = true; + bool useQtVfs = false; #if QT_CONFIG(regularexpression) static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1; bool defineRegexp = false; @@ -708,6 +709,8 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c if (ok) timeOut = nt; } + } else if (option == "QSQLITE_USE_QT_VFS"_L1) { + useQtVfs = true; } else if (option == "QSQLITE_OPEN_READONLY"_L1) { openReadOnlyOption = true; } else if (option == "QSQLITE_OPEN_URI"_L1) { @@ -742,7 +745,7 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c openMode |= SQLITE_OPEN_NOMUTEX; - const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, nullptr); + const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, useQtVfs ? "QtVFS" : nullptr); if (res == SQLITE_OK) { sqlite3_busy_timeout(d->access, timeOut); diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp new file mode 100644 index 0000000000..be5807c6e2 --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp @@ -0,0 +1,255 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsql_sqlite_vfs_p.h" + +#include <QFile> + +#include <limits.h> // defines PATH_MAX on unix +#include <sqlite3.h> +#include <stdio.h> // defines FILENAME_MAX everywhere + +#ifndef PATH_MAX +# define PATH_MAX FILENAME_MAX +#endif + +namespace { +struct Vfs : sqlite3_vfs { + sqlite3_vfs *pVfs; + sqlite3_io_methods ioMethods; +}; + +struct File : sqlite3_file { + class QtFile : public QFile { + public: + QtFile(const QString &name, bool removeOnClose) + : QFile(name) + , removeOnClose(removeOnClose) + {} + + ~QtFile() override + { + if (removeOnClose) + remove(); + } + private: + bool removeOnClose; + }; + QtFile *pFile; +}; + + +int xClose(sqlite3_file *sfile) +{ + auto file = static_cast<File *>(sfile); + delete file->pFile; + file->pFile = nullptr; + return SQLITE_OK; +} + +int xRead(sqlite3_file *sfile, void *ptr, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_READ; + + auto sz = file->pFile->read(static_cast<char *>(ptr), iAmt); + if (sz < iAmt) { + memset(static_cast<char *>(ptr) + sz, 0, size_t(iAmt - sz)); + return SQLITE_IOERR_SHORT_READ; + } + return SQLITE_OK; +} + +int xWrite(sqlite3_file *sfile, const void *data, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_SEEK; + return file->pFile->write(reinterpret_cast<const char*>(data), iAmt) == iAmt ? SQLITE_OK : SQLITE_IOERR_WRITE; +} + +int xTruncate(sqlite3_file *sfile, sqlite3_int64 size) +{ + auto file = static_cast<File *>(sfile); + return file->pFile->resize(size) ? SQLITE_OK : SQLITE_IOERR_TRUNCATE; +} + +int xSync(sqlite3_file *sfile, int /*flags*/) +{ + static_cast<File *>(sfile)->pFile->flush(); + return SQLITE_OK; +} + +int xFileSize(sqlite3_file *sfile, sqlite3_int64 *pSize) +{ + auto file = static_cast<File *>(sfile); + *pSize = file->pFile->size(); + return SQLITE_OK; +} + +// No lock/unlock for QFile, QLockFile doesn't work for me + +int xLock(sqlite3_file *, int) { return SQLITE_OK; } + +int xUnlock(sqlite3_file *, int) { return SQLITE_OK; } + +int xCheckReservedLock(sqlite3_file *, int *pResOut) +{ + *pResOut = 0; + return SQLITE_OK; +} + +int xFileControl(sqlite3_file *, int, void *) { return SQLITE_NOTFOUND; } + +int xSectorSize(sqlite3_file *) +{ + return 4096; +} + +int xDeviceCharacteristics(sqlite3_file *) +{ + return 0; // no SQLITE_IOCAP_XXX +} + +int xOpen(sqlite3_vfs *svfs, sqlite3_filename zName, sqlite3_file *sfile, + int flags, int *pOutFlags) +{ + auto vfs = static_cast<Vfs *>(svfs); + auto file = static_cast<File *>(sfile); + memset(file, 0, sizeof(File)); + QIODeviceBase::OpenMode mode = QIODeviceBase::NotOpen; + if (!zName || (flags & SQLITE_OPEN_MEMORY)) + return SQLITE_PERM; + if ((flags & SQLITE_OPEN_READONLY) && + !(flags & SQLITE_OPEN_READWRITE) && + !(flags & SQLITE_OPEN_CREATE) && + !(flags & SQLITE_OPEN_DELETEONCLOSE)) { + mode |= QIODeviceBase::OpenModeFlag::ReadOnly; + } else { + /* + ** ^The [SQLITE_OPEN_EXCLUSIVE] flag is always used in conjunction + ** with the [SQLITE_OPEN_CREATE] flag, which are both directly + ** analogous to the O_EXCL and O_CREAT flags of the POSIX open() + ** API. The SQLITE_OPEN_EXCLUSIVE flag, when paired with the + ** SQLITE_OPEN_CREATE, is used to indicate that file should always + ** be created, and that it is an error if it already exists. + ** It is <i>not</i> used to indicate the file should be opened + ** for exclusive access. + */ + if ((flags & SQLITE_OPEN_CREATE) && (flags & SQLITE_OPEN_EXCLUSIVE)) + mode |= QIODeviceBase::OpenModeFlag::NewOnly; + + if (flags & SQLITE_OPEN_READWRITE) + mode |= QIODeviceBase::OpenModeFlag::ReadWrite; + } + + file->pMethods = &vfs->ioMethods; + file->pFile = new File::QtFile{QString::fromUtf8(zName), bool(flags & SQLITE_OPEN_DELETEONCLOSE)}; + if (!file->pFile->open(mode)) + return SQLITE_CANTOPEN; + if (pOutFlags) + *pOutFlags = flags; + + return SQLITE_OK; +} + +int xDelete(sqlite3_vfs *, const char *zName, int) +{ + return QFile::remove(QString::fromUtf8(zName)) ? SQLITE_OK : SQLITE_ERROR; +} + +int xAccess(sqlite3_vfs */*svfs*/, const char *zName, int flags, int *pResOut) +{ + *pResOut = 0; + switch (flags) { + case SQLITE_ACCESS_EXISTS: + case SQLITE_ACCESS_READ: + *pResOut = QFile::exists(QString::fromUtf8(zName)); + break; + default: + break; + } + return SQLITE_OK; +} + +int xFullPathname(sqlite3_vfs *, const char *zName, int nOut, char *zOut) +{ + if (!zName) + return SQLITE_ERROR; + + int i = 0; + for (;zName[i] && i < nOut; ++i) + zOut[i] = zName[i]; + + if (i >= nOut) + return SQLITE_ERROR; + + zOut[i] = '\0'; + return SQLITE_OK; +} + +int xRandomness(sqlite3_vfs *svfs, int nByte, char *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xRandomness(vfs, nByte, zOut); +} + +int xSleep(sqlite3_vfs *svfs, int microseconds) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xSleep(vfs, microseconds); +} + +int xCurrentTime(sqlite3_vfs *svfs, double *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTime(vfs, zOut); +} + +int xGetLastError(sqlite3_vfs *, int, char *) +{ + return 0; +} + +int xCurrentTimeInt64(sqlite3_vfs *svfs, sqlite3_int64 *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTimeInt64(vfs, zOut); +} +} // namespace { + +void register_qt_vfs() +{ + static Vfs vfs; + memset(&vfs, 0, sizeof(Vfs)); + vfs.iVersion = 1; + vfs.szOsFile = sizeof(File); + vfs.mxPathname = PATH_MAX; + vfs.zName = "QtVFS"; + vfs.xOpen = &xOpen; + vfs.xDelete = &xDelete; + vfs.xAccess = &xAccess; + vfs.xFullPathname = &xFullPathname; + vfs.xRandomness = &xRandomness; + vfs.xSleep = &xSleep; + vfs.xCurrentTime = &xCurrentTime; + vfs.xGetLastError = &xGetLastError; + vfs.xCurrentTimeInt64 = &xCurrentTimeInt64; + vfs.pVfs = sqlite3_vfs_find(nullptr); + vfs.ioMethods.iVersion = 1; + vfs.ioMethods.xClose = &xClose; + vfs.ioMethods.xRead = &xRead; + vfs.ioMethods.xWrite = &xWrite; + vfs.ioMethods.xTruncate = &xTruncate; + vfs.ioMethods.xSync = &xSync; + vfs.ioMethods.xFileSize = &xFileSize; + vfs.ioMethods.xLock = &xLock; + vfs.ioMethods.xUnlock = &xUnlock; + vfs.ioMethods.xCheckReservedLock = &xCheckReservedLock; + vfs.ioMethods.xFileControl = &xFileControl; + vfs.ioMethods.xSectorSize = &xSectorSize; + vfs.ioMethods.xDeviceCharacteristics = &xDeviceCharacteristics; + + sqlite3_vfs_register(&vfs, 0); +} diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h new file mode 100644 index 0000000000..56024b3ecb --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSQL_SQLITE_VFS_H +#define QSQL_SQLITE_VFS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +void register_qt_vfs(); + + +#endif // QSQL_SQLITE_VFS_H diff --git a/src/plugins/sqldrivers/sqlite/smain.cpp b/src/plugins/sqldrivers/sqlite/smain.cpp index f84a256bc8..0d201c38d3 100644 --- a/src/plugins/sqldrivers/sqlite/smain.cpp +++ b/src/plugins/sqldrivers/sqlite/smain.cpp @@ -4,6 +4,7 @@ #include <qsqldriverplugin.h> #include <qstringlist.h> #include "qsql_sqlite_p.h" +#include "qsql_sqlite_vfs_p.h" QT_BEGIN_NAMESPACE @@ -23,6 +24,7 @@ public: QSQLiteDriverPlugin::QSQLiteDriverPlugin() : QSqlDriverPlugin() { + register_qt_vfs(); } QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) diff --git a/src/sql/doc/src/sql-driver.qdoc b/src/sql/doc/src/sql-driver.qdoc index 97af8c620d..3cac1e8fe3 100644 --- a/src/sql/doc/src/sql-driver.qdoc +++ b/src/sql/doc/src/sql-driver.qdoc @@ -723,6 +723,17 @@ \li Busy handler timeout in milliseconds (val <= 0: disabled), see \l {https://www.sqlite.org/c3ref/busy_timeout.html} {SQLite documentation} for more information + + \row + \li QSQLITE_USE_QT_VFS + \li If set, the database is opened using Qt's VFS which allows to + open databases using QFile. This way it can open databases from + any read-write locations (e.g.android shared storage) but also + from read-only resources (e.g. qrc or android assets). Be aware + that when opening databases from read-only resources make sure + you add QSQLITE_OPEN_READONLY attribute as well. + Otherwise it will fail to open it. + \row \li QSQLITE_OPEN_READONLY \li If set, the database is open in read-only mode which will fail diff --git a/tests/auto/sql/kernel/CMakeLists.txt b/tests/auto/sql/kernel/CMakeLists.txt index 0a2b5dfd42..d51cb75f31 100644 --- a/tests/auto/sql/kernel/CMakeLists.txt +++ b/tests/auto/sql/kernel/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory(qsqlrecord) add_subdirectory(qsqlthread) add_subdirectory(qsql) add_subdirectory(qsqlresult) +add_subdirectory(qvfssql) diff --git a/tests/auto/sql/kernel/qvfssql/CMakeLists.txt b/tests/auto/sql/kernel/qvfssql/CMakeLists.txt new file mode 100644 index 0000000000..184f0a578d --- /dev/null +++ b/tests/auto/sql/kernel/qvfssql/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qsqlfield Test: +##################################################################### + +qt_internal_add_test(tst_qvfssql + SOURCES + tst_qvfssql.cpp + LIBRARIES + Qt::SqlPrivate +) + +set(qvfssql_resource_files + "sample.db" +) + +qt_internal_add_resource(tst_qvfssql "tst_qvfssql" + PREFIX + "/ro/" + FILES + ${qvfssql_resource_files} +) diff --git a/tests/auto/sql/kernel/qvfssql/sample.db b/tests/auto/sql/kernel/qvfssql/sample.db Binary files differnew file mode 100644 index 0000000000..56e6427e3c --- /dev/null +++ b/tests/auto/sql/kernel/qvfssql/sample.db diff --git a/tests/auto/sql/kernel/qvfssql/tst_qvfssql.cpp b/tests/auto/sql/kernel/qvfssql/tst_qvfssql.cpp new file mode 100644 index 0000000000..e2c093c46d --- /dev/null +++ b/tests/auto/sql/kernel/qvfssql/tst_qvfssql.cpp @@ -0,0 +1,94 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QTest> + +#include <qsqldatabase.h> +#include <qstandardpaths.h> + +#include "../qsqldatabase/tst_databases.h" + +using namespace Qt::StringLiterals; + +class tst_QVfsSql : public QObject +{ + Q_OBJECT +private slots: + void testRoDb(); + void testRwDb(); +}; + +void tst_QVfsSql::testRoDb() +{ + QVERIFY(QSqlDatabase::drivers().contains("QSQLITE"_L1)); + QSqlDatabase::addDatabase("QSQLITE"_L1, "ro_db"_L1); + QSqlDatabase db = QSqlDatabase::database("ro_db"_L1, false); + QVERIFY_SQL(db, isValid()); + db.setDatabaseName(":/ro/sample.db"_L1); + + db.setConnectOptions("QSQLITE_USE_QT_VFS"_L1); + QVERIFY(!db.open()); // can not open as the QSQLITE_OPEN_READONLY attribute is missing + + db.setConnectOptions("QSQLITE_USE_QT_VFS;QSQLITE_OPEN_READONLY"_L1); + QVERIFY_SQL(db, open()); + + QStringList tables = db.tables(); + QSqlQuery q{db}; + for (auto table : {"reltest1"_L1, "reltest2"_L1, "reltest3"_L1, "reltest4"_L1, "reltest5"_L1}) { + QVERIFY(tables.contains(table)); + QVERIFY_SQL(q, exec("select * from " + table)); + QVERIFY(q.next()); + } + QVERIFY_SQL(q, exec("select * from reltest1 where id = 4"_L1)); + QVERIFY_SQL(q, first()); + QVERIFY(q.value(0).toInt() == 4); + QVERIFY(q.value(1).toString() == "boris"_L1); + QVERIFY(q.value(2).toInt() == 2); + QVERIFY(q.value(3).toInt() == 2); +} + +void tst_QVfsSql::testRwDb() +{ + QSqlDatabase::addDatabase("QSQLITE"_L1, "rw_db"_L1); + QSqlDatabase db = QSqlDatabase::database("rw_db"_L1, false); + QVERIFY_SQL(db, isValid()); + const auto dbPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/test_qt_vfs.db"_L1; + db.setDatabaseName(dbPath); + QFile::remove(dbPath); + + db.setConnectOptions("QSQLITE_USE_QT_VFS;QSQLITE_OPEN_READONLY"_L1); + QVERIFY(!db.open()); // can not open as the QSQLITE_OPEN_READONLY attribute is set and the file is missing + + db.setConnectOptions("QSQLITE_USE_QT_VFS"_L1); + QVERIFY_SQL(db, open()); + + QVERIFY(db.tables().isEmpty()); + QSqlQuery q{db}; + QVERIFY_SQL(q, exec("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val INTEGER)"_L1)); + QVERIFY_SQL(q, exec("BEGIN"_L1)); + for (int i = 0; i < 1000; ++i) { + q.prepare("INSERT INTO test (val) VALUES (:val)"_L1); + q.bindValue(":val"_L1, i); + QVERIFY_SQL(q, exec()); + } + QVERIFY_SQL(q, exec("COMMIT"_L1)); + QVERIFY_SQL(q, exec("SELECT val FROM test ORDER BY val"_L1)); + for (int i = 0; i < 1000; ++i) { + QVERIFY_SQL(q, next()); + QCOMPARE(q.value(0).toInt() , i); + } + QVERIFY_SQL(q, exec("DELETE FROM test WHERE val < 500"_L1)); + auto fileSize = QFileInfo{dbPath}.size(); + QVERIFY_SQL(q, exec("VACUUM"_L1)); + QVERIFY(QFileInfo{dbPath}.size() < fileSize); // TEST xTruncate VFS + QVERIFY_SQL(q, exec("SELECT val FROM test ORDER BY val"_L1)); + for (int i = 500; i < 1000; ++i) { + QVERIFY_SQL(q, next()); + QCOMPARE(q.value(0).toInt() , i); + } + db.close(); + QFile::remove(dbPath); +} + +QTEST_APPLESS_MAIN(tst_QVfsSql) +#include "tst_qvfssql.moc" |