rssguard/src/librssguard/database/mariadbdriver.cpp
2023-08-11 11:29:09 +02:00

276 lines
9.8 KiB
C++

// For license of this file, see <project-root-folder>/LICENSE.md.
#include "database/mariadbdriver.h"
#include "definitions/definitions.h"
#include "exceptions/applicationexception.h"
#include "miscellaneous/application.h"
#include "miscellaneous/settings.h"
#include <QDir>
#include <QSqlError>
#include <QSqlQuery>
MariaDbDriver::MariaDbDriver(QObject* parent) : DatabaseDriver(parent), m_databaseInitialized(false) {}
QString MariaDbDriver::ddlFilePrefix() const {
return QSL("mysql");
}
MariaDbDriver::MariaDbError MariaDbDriver::testConnection(const QString& hostname,
int port,
const QString& w_database,
const QString& username,
const QString& password) {
QSqlDatabase database = QSqlDatabase::addDatabase(QSL(APP_DB_MYSQL_DRIVER), QSL(APP_DB_MYSQL_TEST));
database.setHostName(hostname);
database.setPort(port);
database.setUserName(username);
database.setPassword(password);
database.setDatabaseName(w_database);
if (database.open() && !database.lastError().isValid()) {
QSqlQuery query(QSL("SELECT version();"), database);
if (!query.lastError().isValid() && query.next()) {
qDebugNN << LOGSEC_DB << "Checked MySQL database, version is" << QUOTE_W_SPACE_DOT(query.value(0).toString());
// Connection succeeded, clean up the mess and return OK status.
database.close();
return MariaDbError::Ok;
}
else {
database.close();
return MariaDbError::UnknownError;
}
}
else if (database.lastError().isValid()) {
auto nat = database.lastError().nativeErrorCode();
bool nat_converted = false;
auto nat_int = nat.toInt(&nat_converted);
if (nat_converted) {
return static_cast<MariaDbError>(nat_int);
}
else {
qWarningNN << LOGSEC_DB << "Failed to recognize MySQL error code:" << QUOTE_W_SPACE_DOT(nat);
return MariaDbError::UnknownError;
}
}
else {
return MariaDbError::UnknownError;
}
}
QString MariaDbDriver::location() const {
return QSL("%1/%2").arg(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLHostname)).toString(),
qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLDatabase)).toString());
}
QString MariaDbDriver::interpretErrorCode(MariaDbDriver::MariaDbError error_code) const {
switch (error_code) {
case MariaDbError::Ok:
return tr("MySQL server works as expected.");
case MariaDbError::UnknownDatabase:
return tr("Selected database does not exist (yet). It will be created. It's okay.");
case MariaDbError::CantConnect:
case MariaDbError::ConnectionError:
case MariaDbError::UnknownHost:
return tr("No MySQL server is running in the target destination.");
case MariaDbError::AccessDenied:
return tr("Access denied. Invalid username or password used.");
default:
return tr("Unknown error: '%1'.").arg(int(error_code));
}
}
QString MariaDbDriver::humanDriverType() const {
return tr("MariaDB");
}
QString MariaDbDriver::qtDriverCode() const {
return QSL(APP_DB_MYSQL_DRIVER);
}
DatabaseDriver::DriverType MariaDbDriver::driverType() const {
return DatabaseDriver::DriverType::MySQL;
}
bool MariaDbDriver::vacuumDatabase() {
QSqlDatabase database = connection(objectName());
QSqlQuery query_vacuum(database);
return query_vacuum.exec(QSL("OPTIMIZE TABLE Feeds;")) && query_vacuum.exec(QSL("OPTIMIZE TABLE Messages;"));
}
bool MariaDbDriver::saveDatabase() {
return true;
}
void MariaDbDriver::backupDatabase(const QString& backup_folder, const QString& backup_name) {
Q_UNUSED(backup_folder)
Q_UNUSED(backup_name)
saveDatabase();
}
bool MariaDbDriver::initiateRestoration(const QString& database_package_file) {
Q_UNUSED(database_package_file)
return true;
}
bool MariaDbDriver::finishRestoration() {
return true;
}
qint64 MariaDbDriver::databaseDataSize() {
QSqlDatabase database = connection(metaObject()->className());
QSqlQuery query(database);
query.prepare("SELECT Round(Sum(data_length + index_length), 1) "
"FROM information_schema.tables "
"WHERE table_schema = :db "
"GROUP BY table_schema;");
query.bindValue(QSL(":db"), database.databaseName());
if (query.exec() && query.next()) {
return query.value(0).value<qint64>();
}
else {
return 0;
}
}
QSqlDatabase MariaDbDriver::initializeDatabase(const QString& connection_name) {
// Folders are created. Create new QSqlDatabase object.
QSqlDatabase database = QSqlDatabase::addDatabase(QSL(APP_DB_MYSQL_DRIVER), connection_name);
const QString database_name = qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLDatabase)).toString();
database.setHostName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLHostname)).toString());
database.setPort(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLPort)).toInt());
database.setUserName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLUsername)).toString());
database.setPassword(qApp->settings()->password(GROUP(Database), SETTING(Database::MySQLPassword)).toString());
if (!database.open()) {
// NOTE: In this case throw exception and fallback SQL backend will be used.
throw ApplicationException(database.lastError().text());
}
else {
QSqlQuery query_db(database);
query_db.setForwardOnly(true);
setPragmas(query_db);
if (!query_db.exec(QSL("USE %1").arg(database_name)) ||
!query_db.exec(QSL("SELECT inf_value FROM Information WHERE inf_key = 'schema_version'"))) {
// If no "rssguard" database exists or schema version is wrong, then initialize it.
qWarningNN << LOGSEC_DB << "Error occurred. MySQL database is not initialized. Initializing now.";
try {
const QStringList statements = prepareScript(APP_SQL_PATH, QSL(APP_DB_MYSQL_INIT), database_name);
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 << "MySQL database backend should be ready now.";
}
else {
// Database was previously initialized. Now just check the schema version.
query_db.next();
const int installed_db_schema = query_db.value(0).toString().toInt();
if (installed_db_schema < QSL(APP_DB_SCHEMA_VERSION).toInt()) {
try {
updateDatabaseSchema(query_db, installed_db_schema, database_name);
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()));
}
}
}
query_db.finish();
}
m_databaseInitialized = true;
return database;
}
void MariaDbDriver::setPragmas(QSqlQuery& query) {
query.exec(QSL("SET NAMES 'utf8mb4';"));
query.exec(QSL("SET CHARACTER SET utf8mb4;"));
}
QSqlDatabase MariaDbDriver::connection(const QString& connection_name,
DatabaseDriver::DesiredStorageType desired_type) {
Q_UNUSED(desired_type)
if (!m_databaseInitialized) {
// Return initialized database.
return initializeDatabase(connection_name);
}
else {
QSqlDatabase database;
if (QSqlDatabase::contains(connection_name)) {
qDebugNN << LOGSEC_DB << "MySQL connection '" << connection_name << "' is already active.";
// This database connection was added previously, no need to
// setup its properties.
database = QSqlDatabase::database(connection_name);
}
else {
// Database connection with this name does not exist
// yet, add it and set it up.
database = QSqlDatabase::addDatabase(QSL(APP_DB_MYSQL_DRIVER), connection_name);
database.setHostName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLHostname)).toString());
database.setPort(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLPort)).toInt());
database.setUserName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLUsername)).toString());
database.setPassword(qApp->settings()->password(GROUP(Database), SETTING(Database::MySQLPassword)).toString());
database.setDatabaseName(qApp->settings()->value(GROUP(Database), SETTING(Database::MySQLDatabase)).toString());
}
if (!database.isOpen() && !database.open()) {
// NOTE: In this case throw exception and fallback SQL backend will be used.
throw ApplicationException(database.lastError().text());
}
else {
qDebugNN << LOGSEC_DB << "MySQL database connection" << QUOTE_W_SPACE(connection_name) << "to file"
<< QUOTE_W_SPACE(QDir::toNativeSeparators(database.databaseName())) << "seems to be established.";
}
QSqlQuery query_db(database);
query_db.setForwardOnly(true);
setPragmas(query_db);
return database;
}
}
QString MariaDbDriver::autoIncrementPrimaryKey() const {
return QSL("INTEGER AUTO_INCREMENT PRIMARY KEY");
}
QString MariaDbDriver::blob() const {
return QSL("MEDIUMBLOB");
}