// For license of this file, see /LICENSE.md. #include "database/sqlitedriver.h" #include "exceptions/applicationexception.h" #include "exceptions/ioexception.h" #include "miscellaneous/application.h" #include #include #include SqliteDriver::SqliteDriver(bool in_memory, QObject* parent) : DatabaseDriver(parent), m_inMemoryDatabase(in_memory), m_databaseFilePath(qApp->userDataFolder() + QDir::separator() + QString(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")); } 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); QSqlDatabase file_database = connection(QSL("SaveToFile"), DatabaseDriver::DesiredStorageType::StrictlyFileBased); 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 into 'main." << table << "'."; } else { qCriticalNN << LOGSEC_DB << "Failed to copy new data to '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(APP_DB_SQLITE_DRIVER, connection_name); if (want_in_memory) { database.setConnectOptions(QSL("QSQLITE_OPEN_URI;QSQLITE_ENABLE_SHARED_CACHE")); database.setDatabaseName(QSL("file::memory:")); } else { const QDir db_path(m_databaseFilePath); QFile db_file(db_path.absoluteFilePath(APP_DB_SQLITE_FILE)); 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(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(APP_DB_SQLITE_DRIVER, connection_name); if (in_memory) { database.setConnectOptions(QSL("QSQLITE_OPEN_URI;QSQLITE_ENABLE_SHARED_CACHE")); } 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, APP_DB_SQLITE_INIT); for (const QString& statement : statements) { query_db.exec(statement); if (query_db.lastError().isValid()) { throw ApplicationException(query_db.lastError().text()); } } } 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 QString installed_db_schema = query_db.value(0).toString(); if (installed_db_schema.toInt() < QString(APP_DB_SCHEMA_VERSION).toInt()) { if (updateDatabaseSchema(database, installed_db_schema)) { qDebugNN << LOGSEC_DB << "Database schema was updated from '" << installed_db_schema << "' to '" << APP_DB_SCHEMA_VERSION << "' successully or it is already up to date."; } else { qFatal("Database schema was not updated from '%s' to '%s' successully.", qPrintable(installed_db_schema), APP_DB_SCHEMA_VERSION); } } 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(QString("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(QString("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'")); } // 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; } bool SqliteDriver::updateDatabaseSchema(const QSqlDatabase& database, const QString& source_db_schema_version) { int working_version = QString(source_db_schema_version).remove('.').toInt(); const int current_version = QString(APP_DB_SCHEMA_VERSION).remove('.').toInt(); // Now, it would be good to create backup of SQLite DB file. if (IOFactory::copyFile(databaseFilePath(), databaseFilePath() + ".bak")) { qDebugNN << LOGSEC_DB << "Creating backup of SQLite DB file."; } else { qFatal("Creation of backup SQLite DB file failed."); } while (working_version != current_version) { try { const QStringList statements = prepareScript(APP_SQL_PATH, QString(APP_DB_UPDATE_FILE_PATTERN).arg(QSL("sqlite"), QString::number(working_version), QString::number(working_version + 1))); for (const QString& statement : statements) { QSqlQuery query = database.exec(statement); if (!query.exec(statement) && query.lastError().isValid()) { throw ApplicationException(query.lastError().text()); } } } catch (const ApplicationException& ex) { qFatal("Error when running SQL scripts: %s.", qPrintable(ex.message())); } // Increment the version. qDebugNN << LOGSEC_DB << "Updating database schema:" << QUOTE_W_SPACE(working_version) << "->" << QUOTE_W_SPACE_DOT(working_version + 1); working_version++; } return true; } 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")); } 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(); } else { return 0; } if (query.exec(QSL("PRAGMA page_size;"))) { query.next(); result *= query.value(0).value(); } else { return 0; } return result; } QString SqliteDriver::humanDriverType() const { return tr("SQLite (embedded database)"); } QString SqliteDriver::qtDriverCode() const { return APP_DB_SQLITE_DRIVER; } void SqliteDriver::backupDatabase(const QString& backup_folder, const QString& backup_name) { 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"); }