445 lines
15 KiB
C++
Executable File
445 lines
15 KiB
C++
Executable File
// For license of this file, see <project-root-folder>/LICENSE.md.
|
|
|
|
#include "database/sqlitedriver.h"
|
|
|
|
#include "exceptions/applicationexception.h"
|
|
#include "exceptions/ioexception.h"
|
|
#include "miscellaneous/application.h"
|
|
|
|
#include <QDir>
|
|
#include <QSqlError>
|
|
#include <QSqlQuery>
|
|
|
|
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<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 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");
|
|
}
|