rssguard/src/librssguard/database/sqlitedriver.cpp
2023-08-16 20:44:38 +02:00

454 lines
16 KiB
C++

// For license of this file, see <project-root-folder>/LICENSE.md.
#include "database/sqlitedriver.h"
#include "exceptions/applicationexception.h"
#include "miscellaneous/application.h"
#if defined(SYSTEM_SQLITE3)
#include <sqlite3.h>
#else
#include "3rd-party/sqlite/sqlite3.h"
#endif
#include <QDir>
#include <QSqlDriver>
#include <QSqlError>
#include <QSqlQuery>
SqliteDriver::SqliteDriver(bool in_memory, QObject* parent)
: DatabaseDriver(parent), m_inMemoryDatabase(in_memory),
m_databaseFilePath(qApp->userDataFolder() + QDir::separator() + QSL(APP_DB_SQLITE_PATH)),
m_fileBasedDatabaseInitialized(false), m_inMemoryDatabaseInitialized(false) {}
QString SqliteDriver::location() const {
return QDir::toNativeSeparators(m_databaseFilePath);
}
DatabaseDriver::DriverType SqliteDriver::driverType() const {
return DriverType::SQLite;
}
bool SqliteDriver::vacuumDatabase() {
QSqlDatabase database;
saveDatabase();
database = connection(objectName(), DatabaseDriver::DesiredStorageType::StrictlyFileBased);
QSqlQuery query_vacuum(database);
return query_vacuum.exec(QSL("VACUUM"));
}
QString SqliteDriver::ddlFilePrefix() const {
return QSL("sqlite");
}
int loadOrSaveDb(sqlite3* pInMemory, const char* zFilename, int isSave) {
int rc; /* Function return code */
sqlite3* pFile; /* Database connection opened on zFilename */
sqlite3_backup* pBackup; /* Backup object used to copy data */
sqlite3* pTo; /* Database to copy to (pFile or pInMemory) */
sqlite3* pFrom; /* Database to copy from (pFile or pInMemory) */
/* Open the database file identified by zFilename. Exit early if this fails
** for any reason. */
rc = sqlite3_open(zFilename, &pFile);
if (rc == SQLITE_OK) {
/* If this is a 'load' operation (isSave==0), then data is copied
** from the database file just opened to database pInMemory.
** Otherwise, if this is a 'save' operation (isSave==1), then data
** is copied from pInMemory to pFile. Set the variables pFrom and
** pTo accordingly. */
pFrom = (isSave ? pInMemory : pFile);
pTo = (isSave ? pFile : pInMemory);
/* Set up the backup procedure to copy from the "main" database of
** connection pFile to the main database of connection pInMemory.
** If something goes wrong, pBackup will be set to NULL and an error
** code and message left in connection pTo.
**
** If the backup object is successfully created, call backup_step()
** to copy data from pFile to pInMemory. Then call backup_finish()
** to release resources associated with the pBackup object. If an
** error occurred, then an error code and message will be left in
** connection pTo. If no error occurred, then the error code belonging
** to pTo is set to SQLITE_OK.
*/
pBackup = sqlite3_backup_init(pTo, "main", pFrom, "main");
if (pBackup) {
(void)sqlite3_backup_step(pBackup, -1);
(void)sqlite3_backup_finish(pBackup);
}
rc = sqlite3_errcode(pTo);
}
sqlite3_db_cacheflush(pFile);
/* Close the database connection opened on database file zFilename
** and return the result of this function. */
(void)sqlite3_close(pFile);
return rc;
}
bool SqliteDriver::saveDatabase() {
if (!m_inMemoryDatabase) {
return true;
}
qDebugNN << LOGSEC_DB << "Saving in-memory working database back to persistent file-based storage.";
QSqlDatabase database = connection(QSL("SaveFromMemory"), DatabaseDriver::DesiredStorageType::StrictlyInMemory);
const QDir db_path(m_databaseFilePath);
QFile db_file(db_path.absoluteFilePath(QSL(APP_DB_SQLITE_FILE)));
QVariant v = database.driver()->handle();
if (v.isValid() && (qstrcmp(v.typeName(), "sqlite3*") == 0)) {
// v.data() returns a pointer to the handle
sqlite3* handle = *static_cast<sqlite3**>(v.data());
if (handle) {
loadOrSaveDb(handle, QDir::toNativeSeparators(db_file.fileName()).toStdString().c_str(), 1);
}
}
return true;
/*
QSqlQuery copy_contents(database);
// Attach database.
copy_contents.exec(QString(QSL("ATTACH DATABASE '%1' AS 'storage';")).arg(file_database.databaseName()));
// Copy all stuff.
QStringList tables;
if (copy_contents.exec(QSL("SELECT name FROM storage.sqlite_master WHERE type='table';"))) {
while (copy_contents.next()) {
tables.append(copy_contents.value(0).toString());
}
}
else {
qFatal("Cannot obtain list of table names from file-base SQLite database.");
}
for (const QString& table : tables) {
if (copy_contents.exec(QString(QSL("DELETE FROM storage.%1;")).arg(table))) {
qDebugNN << LOGSEC_DB << "Cleaning old data from 'storage." << table << "'.";
}
else {
qCriticalNN << LOGSEC_DB << "Failed to clean old data from 'storage." << table << "', error: '"
<< copy_contents.lastError().text() << "'.";
}
if (copy_contents.exec(QString(QSL("INSERT INTO storage.%1 SELECT * FROM main.%1;")).arg(table))) {
qDebugNN << LOGSEC_DB << "Copying new data from 'main." << table << "'.";
}
else {
qCriticalNN << LOGSEC_DB << "Failed to copy new data from 'main." << table << "', error: '"
<< copy_contents.lastError().text() << "'.";
}
}
// Detach database and finish.
if (copy_contents.exec(QSL("DETACH 'storage'"))) {
qDebugNN << LOGSEC_DB << "Detaching persistent SQLite file.";
}
else {
qCriticalNN << LOGSEC_DB << "Failed to detach SQLite file, error: '" << copy_contents.lastError().text() << "'.";
}
copy_contents.finish();
return true;*/
}
QSqlDatabase SqliteDriver::connection(const QString& connection_name, DesiredStorageType desired_type) {
bool want_in_memory = desired_type == DatabaseDriver::DesiredStorageType::StrictlyInMemory ||
(desired_type == DatabaseDriver::DesiredStorageType::FromSettings && m_inMemoryDatabase);
if ((want_in_memory && !m_inMemoryDatabaseInitialized) || (!want_in_memory && !m_fileBasedDatabaseInitialized)) {
return initializeDatabase(connection_name, want_in_memory);
}
else {
// No need to initialize.
QSqlDatabase database;
if (QSqlDatabase::contains(connection_name)) {
qDebugNN << LOGSEC_DB << "SQLite connection" << QUOTE_W_SPACE(connection_name) << "is already active.";
// This database connection was added previously, no need to
// setup its properties.
database = QSqlDatabase::database(connection_name);
}
else {
database = QSqlDatabase::addDatabase(QSL(APP_DB_SQLITE_DRIVER), connection_name);
if (want_in_memory) {
database.setConnectOptions(QSL("QSQLITE_OPEN_URI;QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP"));
database.setDatabaseName(QSL("file::memory:"));
}
else {
const QDir db_path(m_databaseFilePath);
QFile db_file(db_path.absoluteFilePath(QSL(APP_DB_SQLITE_FILE)));
database.setConnectOptions(QSL("QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP"));
database.setDatabaseName(db_file.fileName());
}
}
if (!database.isOpen() && !database.open()) {
qFatal("SQLite database was NOT opened. Delivered error message: '%s'.", qPrintable(database.lastError().text()));
}
else {
qDebugNN << LOGSEC_DB << "SQLite database connection" << QUOTE_W_SPACE(connection_name) << "to file"
<< QUOTE_W_SPACE(database.databaseName()) << "seems to be established.";
}
QSqlQuery query_db(database);
query_db.setForwardOnly(true);
setPragmas(query_db);
return database;
}
}
bool SqliteDriver::initiateRestoration(const QString& database_package_file) {
return IOFactory::copyFile(database_package_file,
m_databaseFilePath + QDir::separator() + BACKUP_NAME_DATABASE + BACKUP_SUFFIX_DATABASE);
}
bool SqliteDriver::finishRestoration() {
const QString backup_database_file =
m_databaseFilePath + QDir::separator() + BACKUP_NAME_DATABASE + BACKUP_SUFFIX_DATABASE;
if (QFile::exists(backup_database_file)) {
qDebugNN << LOGSEC_DB << "Backup database file '" << QDir::toNativeSeparators(backup_database_file)
<< "' was detected. Restoring it.";
if (IOFactory::copyFile(backup_database_file, m_databaseFilePath + QDir::separator() + APP_DB_SQLITE_FILE)) {
QFile::remove(backup_database_file);
qDebugNN << LOGSEC_DB << "Database file was restored successully.";
}
else {
qCriticalNN << LOGSEC_DB << "Database file was NOT restored due to error when copying the file.";
return false;
}
}
return true;
}
QSqlDatabase SqliteDriver::initializeDatabase(const QString& connection_name, bool in_memory) {
finishRestoration();
QString db_file_name;
if (!in_memory) {
// Prepare file paths.
const QDir db_path(m_databaseFilePath);
QFile db_file(db_path.absoluteFilePath(QSL(APP_DB_SQLITE_FILE)));
// Check if database directory exists.
if (!db_path.exists()) {
if (!db_path.mkpath(db_path.absolutePath())) {
// Failure when create database file path.
qFatal("Directory '%s' for SQLite database file '%s' was NOT created."
"This is HUGE problem.",
qPrintable(db_path.absolutePath()),
qPrintable(db_file.symLinkTarget()));
}
}
db_file_name = db_file.fileName();
}
else {
db_file_name = QSL("file::memory:");
}
// Folders are created. Create new QSQLDatabase object.
QSqlDatabase database;
database = QSqlDatabase::addDatabase(QSL(APP_DB_SQLITE_DRIVER), connection_name);
if (in_memory) {
database.setConnectOptions(QSL("QSQLITE_OPEN_URI;QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP"));
}
else {
database.setConnectOptions(QSL("QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP"));
}
database.setDatabaseName(db_file_name);
if (!database.open()) {
qFatal("SQLite database was NOT opened. Delivered error message: '%s'", qPrintable(database.lastError().text()));
}
else {
QSqlQuery query_db(database);
query_db.setForwardOnly(true);
setPragmas(query_db);
// Sample query which checks for existence of tables.
if (!query_db.exec(QSL("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'"))) {
qWarningNN << LOGSEC_DB << "SQLite database is not initialized. Initializing now.";
try {
const QStringList statements = prepareScript(APP_SQL_PATH, QSL(APP_DB_SQLITE_INIT));
for (const QString& statement : statements) {
query_db.exec(statement);
if (query_db.lastError().isValid()) {
throw ApplicationException(query_db.lastError().text());
}
}
setSchemaVersion(query_db, QSL(APP_DB_SCHEMA_VERSION).toInt(), true);
}
catch (const ApplicationException& ex) {
qFatal("Error when running SQL scripts: %s.", qPrintable(ex.message()));
}
qDebugNN << LOGSEC_DB << "SQLite database backend should be ready now.";
}
else if (!in_memory) {
query_db.next();
const int installed_db_schema = query_db.value(0).toString().toInt();
if (installed_db_schema < QSL(APP_DB_SCHEMA_VERSION).toInt()) {
// Now, it would be good to create backup of SQLite DB file.
if (IOFactory::copyFile(databaseFilePath(), databaseFilePath() + QSL("-v%1.bak").arg(installed_db_schema))) {
qDebugNN << LOGSEC_DB << "Creating backup of SQLite DB file.";
}
else {
qFatal("Creation of backup SQLite DB file failed.");
}
try {
updateDatabaseSchema(query_db, installed_db_schema);
qDebugNN << LOGSEC_DB << "Database schema was updated from" << QUOTE_W_SPACE(installed_db_schema) << "to"
<< QUOTE_W_SPACE(APP_DB_SCHEMA_VERSION) << "successully.";
}
catch (const ApplicationException& ex) {
qFatal("Error when updating DB schema from %d: %s.", installed_db_schema, qPrintable(ex.message()));
}
}
qDebugNN << LOGSEC_DB << "File-based SQLite database connection '" << connection_name << "' to file '"
<< QDir::toNativeSeparators(database.databaseName()) << "' seems to be established.";
qDebugNN << LOGSEC_DB << "File-based SQLite database has version '" << installed_db_schema << "'.";
}
else {
query_db.next();
qDebugNN << LOGSEC_DB << "SQLite database has version" << QUOTE_W_SPACE_DOT(query_db.value(0).toString());
}
}
if (in_memory) {
// Loading messages from file-based database.
QSqlDatabase file_database = connection(objectName(), DatabaseDriver::DesiredStorageType::StrictlyFileBased);
QSqlQuery copy_contents(database);
// Attach database.
copy_contents.exec(QSL("ATTACH DATABASE '%1' AS 'storage';").arg(file_database.databaseName()));
// Copy all stuff.
QStringList tables;
if (copy_contents.exec(QSL("SELECT name FROM storage.sqlite_master WHERE type = 'table';"))) {
while (copy_contents.next()) {
tables.append(copy_contents.value(0).toString());
}
}
else {
qFatal("Cannot obtain list of table names from file-based SQLite database.");
}
for (const QString& table : tables) {
copy_contents.exec(QSL("INSERT INTO main.%1 SELECT * FROM storage.%1;").arg(table));
}
qDebugNN << LOGSEC_DB << "Copying data from file-based database into working in-memory database.";
// Detach database and finish.
copy_contents.exec(QSL("DETACH 'storage'"));
file_database.close();
QSqlDatabase::removeDatabase(file_database.connectionName());
}
// Everything is initialized now.
if (in_memory) {
m_inMemoryDatabaseInitialized = true;
}
else {
m_fileBasedDatabaseInitialized = true;
}
return database;
}
QString SqliteDriver::databaseFilePath() const {
return m_databaseFilePath + QDir::separator() + APP_DB_SQLITE_FILE;
}
void SqliteDriver::setPragmas(QSqlQuery& query) {
query.exec(QSL("PRAGMA encoding = \"UTF-8\""));
query.exec(QSL("PRAGMA synchronous = OFF"));
// query.exec(QSL("PRAGMA journal_mode = MEMORY"));
query.exec(QSL("PRAGMA page_size = 4096"));
query.exec(QSL("PRAGMA cache_size = 16384"));
query.exec(QSL("PRAGMA count_changes = OFF"));
query.exec(QSL("PRAGMA temp_store = MEMORY"));
query.exec(QSL("PRAGMA journal_mode = WAL"));
}
qint64 SqliteDriver::databaseDataSize() {
QSqlDatabase database = connection(metaObject()->className(), DatabaseDriver::DesiredStorageType::FromSettings);
qint64 result = 1;
QSqlQuery query(database);
if (query.exec(QSL("PRAGMA page_count;"))) {
query.next();
result *= query.value(0).value<qint64>();
}
else {
return 0;
}
if (query.exec(QSL("PRAGMA page_size;"))) {
query.next();
result *= query.value(0).value<qint64>();
}
else {
return 0;
}
return result;
}
QString SqliteDriver::humanDriverType() const {
return tr("SQLite (embedded database)");
}
QString SqliteDriver::qtDriverCode() const {
return QSL(APP_DB_SQLITE_DRIVER);
}
void SqliteDriver::backupDatabase(const QString& backup_folder, const QString& backup_name) {
saveDatabase();
if (!IOFactory::copyFile(databaseFilePath(),
backup_folder + QDir::separator() + backup_name + BACKUP_SUFFIX_DATABASE)) {
throw ApplicationException(tr("Database file not copied to output directory successfully."));
}
}
QString SqliteDriver::autoIncrementPrimaryKey() const {
return QSL("INTEGER PRIMARY KEY");
}
QString SqliteDriver::blob() const {
return QSL("BLOB");
}