/*
*openPilotLog - A FOSS Pilot Logbook Application
*Copyright (C) 2020-2023 Felix Turowsky
*
*This program is free software: you can redistribute it and/or modify
*it under the terms of the GNU General Public License as published by
*the Free Software Foundation, either version 3 of the License, or
*(at your option) any later version.
*
*This program is distributed in the hope that it will be useful,
*but WITHOUT ANY WARRANTY; without even the implied warranty of
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
*GNU General Public License for more details.
*
*You should have received a copy of the GNU General Public License
*along with this program. If not, see .
*/
#include "database.h"
#include "src/opl.h"
#include "src/classes/jsonhelper.h"
namespace OPL {
bool Database::connect()
{
if (!QSqlDatabase::isDriverAvailable(SQLITE_DRIVER)) {
LOG << "Error: No SQLITE Driver availabe.";
return false;
}
QSqlDatabase db = QSqlDatabase::addDatabase(SQLITE_DRIVER);
db.setDatabaseName(databaseFile.absoluteFilePath());
if (!db.open()) {
LOG << QString("Unable to establish database connection.
The following error has ocurred:
%1")
.arg(db.lastError().databaseText());
lastError = db.lastError();
return false;
}
LOG << "Database connection established: " + databaseFile.absoluteFilePath();
// Enable foreign key restrictions
QSqlQuery query;
query.prepare(QStringLiteral("PRAGMA foreign_keys = ON;"));
query.exec();
updateLayout();
return true;
}
void Database::disconnect()
{
QString connection_name;
{
auto db = Database::database();
connection_name = db.connectionName();
db.close();
}
QSqlDatabase::removeDatabase(connection_name);
LOG << "Database connection closed.";
}
const QList &Database::getTemplateTables() const
{
return TEMPLATE_TABLES;
}
const QList &Database::getUserTables() const
{
return USER_TABLES;
}
const QStringList Database::getTableColumns(OPL::DbTable table_name) const
{
return tableColumns.value(OPL::GLOBALS->getDbTableName(table_name));
}
const QStringList Database::getTableNames() const
{
return tableNames;
}
void Database::updateLayout()
{
auto db = Database::database();
tableNames = db.tables();
tableColumns.clear();
for (const auto &table_name : std::as_const(tableNames)) {
QStringList table_columns;
QSqlRecord fields = db.record(table_name);
for (int i = 0; i < fields.count(); i++) {
table_columns.append(fields.field(i).name());
}
tableColumns.insert(table_name, table_columns);
}
emit dataBaseUpdated(DbTable::Any);
}
const QString Database::sqliteVersion() const
{
QSqlQuery query;
query.prepare(QStringLiteral("SELECT sqlite_version()"));
query.exec();
query.next();
return query.value(0).toString();
}
QSqlDatabase Database::database()
{
return QSqlDatabase::database(QStringLiteral("qt_sql_default_connection"));
}
bool Database::commit(const OPL::Row &row)
{
if (!row.isValid())
return false;
if (exists(row))
return update(row);
else
return insert(row);
}
bool Database::commit(const QJsonArray &json_arr, const OPL::DbTable table)
{
// create statement
const QString table_name = OPL::GLOBALS->getDbTableName(table);
QString statement = QLatin1String("INSERT INTO ") + table_name + QLatin1String(" (");
QString placeholder = QStringLiteral(") VALUES (");
for (const auto &column_name : DB->getTableColumns(table)) {
statement += column_name + ',';
placeholder.append(QLatin1Char(':') + column_name + QLatin1Char(','));
}
statement.chop(1);
placeholder.chop(1);
placeholder.append(')');
statement.append(placeholder);
// Create query and commit
QSqlQuery q;
q.prepare(QStringLiteral("BEGIN EXCLUSIVE TRANSACTION"));
q.exec();
for (const auto &entry : json_arr) {
q.prepare(statement);
auto object = entry.toObject();
const auto keys = object.keys();
for (const auto &key : keys){
//use QMetaType for binding null value in QT >= 6
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
object.value(key).isNull() ? q.bindValue(key, QVariant(QMetaType(QMetaType::Int))) :
#else
object.value(key).isNull() ? q.bindValue(key, QVariant(QVariant::String)) :
#endif
q.bindValue(QLatin1Char(':') + key, object.value(key).toVariant());
}
q.exec();
}
q.prepare(QStringLiteral("COMMIT"));
if (q.exec())
return true;
else
return false;
}
bool Database::remove(const OPL::Row &row)
{
if (!exists(row)) {
LOG << "Error: Database entry not found.";
return false;
}
const QString table_name = OPL::GLOBALS->getDbTableName(row.getTable());
QString statement = QLatin1String("DELETE FROM ") + table_name
+ QLatin1String(" WHERE ROWID=?");
QSqlQuery query;
query.prepare(statement);
query.addBindValue(row.getRowId());
if (query.exec())
{
LOG << "Entry removed:";
LOG << row;
emit dataBaseUpdated(row.getTable());
return true;
} else {
DEB << "Unable to delete.";
DEB << "Query: " << statement;
DEB << "Query Error: " << query.lastError().text();
lastError = query.lastError();
return false;
}
}
bool Database::removeMany(OPL::DbTable table, const QList &row_id_list)
{
const QString table_name = OPL::GLOBALS->getDbTableName(table);
int errorCount = 0;
QSqlQuery query;
query.prepare(QStringLiteral("BEGIN EXCLUSIVE TRANSACTION"));
query.exec();
for (const auto row_id : row_id_list) {
const QString statement = QLatin1String("DELETE FROM ") + table_name +
QLatin1String(" WHERE ROWID=?");
query.prepare(statement);
query.addBindValue(row_id);
if (!query.exec())
errorCount++;
}
if (errorCount == 0) {
query.prepare(QStringLiteral("COMMIT"));
if(query.exec()) {
emit dataBaseUpdated(table);
LOG << "Transaction successfull.";
return true;
} else {
LOG << "Transaction unsuccessful (Interrupted). Error count: "
+ QString::number(errorCount);
DEB << query.lastError().text();
lastError = query.lastError();
return false;
}
} else {
query.prepare(QStringLiteral("ROLLBACK"));
query.exec();
LOG << "Transaction unsuccessful (no changes have been made). Error count: "
+ QString::number(errorCount);
return false;
}
}
bool Database::exists(const OPL::Row &row)
{
if (row.getRowId() == 0)
return false;
//Check database for row id
QString statement = QLatin1String("SELECT COUNT(*) FROM ") + OPL::GLOBALS->getDbTableName(row.getTable())
+ QLatin1String(" WHERE ROWID=?");
QSqlQuery query;
query.prepare(statement);
query.addBindValue(row.getRowId());
query.setForwardOnly(true);
query.exec();
//this returns either 1 or 0 since row ids are unique
if (!query.isActive()) {
lastError = query.lastError();
DEB << "Query Error: " << query.lastError().text() << statement;
return false;
}
query.next();
int rowId = query.value(0).toInt();
if (rowId) {
return true;
} else {
LOG << "Database entry not found.";
return false;
}
}
bool Database::clear()
{
QSqlQuery q;
for (const auto &table : USER_TABLES) {
q.prepare(QLatin1String("DELETE FROM ") + OPL::GLOBALS->getDbTableName(table));
if (!q.exec()) {
DEB << "Error: " << q.lastError().text();
lastError = q.lastError();
return false;
}
}
return true;
}
bool Database::update(const OPL::Row &updated_row)
{
QString statement = QLatin1String("UPDATE ") + OPL::GLOBALS->getDbTableName(updated_row.getTable()) + QLatin1String(" SET ");
const auto& data = updated_row.getData();
for (auto i = data.constBegin(); i != data.constEnd(); ++i) {
statement.append(i.key() + "=?,");
}
statement.chop(1);
statement.append(QLatin1String(" WHERE ROWID=?"));
QSqlQuery query;
query.prepare(statement);
DEB << "Statement: " << statement;
for (auto i = data.constBegin(); i != data.constEnd(); ++i) {
//use QMetaType for binding null value in QT >= 6
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (i.value() == QVariant(QString())) {
query.addBindValue(QVariant(QMetaType(QMetaType::Int)));
#else
if (i.value() == QVariant(QString())) {
query.addBindValue(QVariant(QVariant::String));
#endif
} else {
query.addBindValue(i.value());
}
}
query.addBindValue(updated_row.getRowId());
DEB << "Bound values: " << query.boundValues();
if (query.exec())
{
LOG << QString("Entry successfully committed. %1").arg(updated_row.getPosition());
emit dataBaseUpdated(updated_row.getTable());
return true;
} else {
DEB << "Unable to commit.";
DEB << "Query: " << statement;
DEB << "Query Error: " << query.lastError().text();
lastError = query.lastError();
return false;
}
}
bool Database::insert(const OPL::Row &new_row)
{
QString statement = QLatin1String("INSERT INTO ") + OPL::GLOBALS->getDbTableName(new_row.getTable()) + QLatin1String(" (");
const auto& data = new_row.getData();
QHash::const_iterator i;
for (i = data.constBegin(); i != data.constEnd(); ++i) {
statement.append(i.key() + QLatin1Char(','));
}
statement.chop(1);
statement += QLatin1String(") VALUES (");
for (int i=0; i < new_row.getData().size(); ++i) {
statement += QLatin1String("?,");
}
statement.chop(1);
statement += QLatin1Char(')');
QSqlQuery query;
query.prepare(statement);
for (i = data.constBegin(); i != data.constEnd(); ++i) {
//use QMetaType for binding null value in QT >= 6
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (i.value() == QVariant(QString())) {
query.addBindValue(QVariant(QMetaType(QMetaType::Int)));
#else
if (i.value() == QVariant(QString())) {
query.addBindValue(QVariant(QVariant::String));
#endif
} else {
query.addBindValue(i.value());
}
}
//check result.
if (query.exec())
{
LOG << QString("Entry successfully committed. %1").arg(new_row.getPosition());
emit dataBaseUpdated(new_row.getTable());
return true;
} else {
DEB << "Unable to commit.";
DEB << "Query: " << statement;
DEB << "Bound Values: " << query.boundValues();
DEB << "Query Error: " << query.lastError().text();
lastError = query.lastError();
return false;
}
}
OPL::Row Database::getRow(const OPL::DbTable table, const int row_id)
{
QString statement = QLatin1String("SELECT * FROM ") + OPL::GLOBALS->getDbTableName(table)
+ QLatin1String(" WHERE ROWID=?");
QSqlQuery q;
q.prepare(statement);
q.addBindValue(row_id);
q.setForwardOnly(true);
if (!q.exec()) {
DEB << "SQL error: " << q.lastError().text();
DEB << "Statement: " << q.lastQuery();
lastError = q.lastError();
return {}; // return invalid Row
}
RowData_T entry_data;
if(q.next()) {
auto r = q.record(); // retreive record
if (r.count() == 0) // row is empty
return {};
for (int i = 0; i < r.count(); i++){ // iterate through fields to get key:value map
if(!r.value(i).isNull()) {
entry_data.insert(r.fieldName(i), r.value(i));
}
}
}
return OPL::Row(table, row_id, entry_data);
}
RowData_T Database::getRowData(const OPL::DbTable table, const int row_id)
{
QString statement = QLatin1String("SELECT * FROM ") + OPL::GLOBALS->getDbTableName(table)
+ QLatin1String(" WHERE ROWID=?");
QSqlQuery q;
q.prepare(statement);
q.addBindValue(row_id);
q.setForwardOnly(true);
if (!q.exec()) {
DEB << "SQL error: " << q.lastError().text();
DEB << "Statement: " << q.lastQuery();
lastError = q.lastError();
return {}; // return invalid Row
}
RowData_T entry_data;
if(q.next()) {
auto r = q.record(); // retreive record
if (r.count() == 0) // row is empty
return {};
for (int i = 0; i < r.count(); i++){ // iterate through fields to get key:value map
if(!r.value(i).isNull()) {
entry_data.insert(r.fieldName(i), r.value(i));
}
}
}
return entry_data;
}
int Database::getLastEntry(OPL::DbTable table)
{
QString statement = QLatin1String("SELECT MAX(ROWID) FROM ") + OPL::GLOBALS->getDbTableName(table);
auto query = QSqlQuery(statement);
if (query.first()) {
return query.value(0).toInt();
} else {
LOG << "No entry found. (Database empty?)" << query.lastError().text();
return 0;
}
}
const RowData_T Database::getTotals(bool includePreviousExperience)
{
QString statement = "SELECT"
" SUM(tblk) AS tblk,"
" SUM(tSPSE) AS tSPSE,"
" SUM(tSPME) AS tSPME,"
" SUM(tMP) AS tMP,"
" SUM(tPIC) AS tPIC,"
" SUM(tSIC) AS tSIC,"
" SUM(tDUAL) AS tDUAL,"
" SUM(tFI) AS tFI,"
" SUM(tPICUS) AS tPICUS,"
" SUM(tNIGHT) AS tNIGHT,"
" SUM(tIFR) AS tIFR,"
" SUM(tSIM) AS tSIM,"
" SUM(toDay) AS toDay,"
" SUM(toNight) AS toNight,"
" SUM(ldgDay) AS ldgDay,"
" SUM(ldgNight) AS ldgNight"
" FROM flights";
QSqlQuery query;
query.prepare(statement);
if (!query.exec()) {
DEB << "SQL error: " << query.lastError().text();
DEB << "Statement: " << query.lastQuery();
lastError = query.lastError();
return {}; // return invalid Row
}
RowData_T entry_data;
if(query.next()) {
auto r = query.record(); // retreive record
if (r.count() == 0) // row is empty
return {};
for (int i = 0; i < r.count(); i++){ // iterate through fields to get key:value map
if(!r.value(i).isNull()) {
entry_data.insert(r.fieldName(i), r.value(i));
}
}
}
if(!includePreviousExperience) {
return entry_data;
}
// name the return types for easy mapping to QLineEdit names
statement = "SELECT"
" SUM(tblk) AS tblk,"
" SUM(tSPSE) AS tSPSE,"
" SUM(tSPME) AS tSPME,"
" SUM(tMP) AS tMP,"
" SUM(tPIC) AS tPIC,"
" SUM(tSIC) AS tSIC,"
" SUM(tDUAL) AS tDUAL,"
" SUM(tFI) AS tFI,"
" SUM(tPICUS) AS tPICUS,"
" SUM(tNIGHT) AS tNIGHT,"
" SUM(tIFR) AS tIFR,"
" SUM(tSIM) AS tSIM,"
" SUM(toDay) AS toDay,"
" SUM(toNight) AS toNight,"
" SUM(ldgDay) AS ldgDay,"
" SUM(ldgNight) AS ldgNight"
" FROM previousExperience";
query.prepare(statement);
if (!query.exec()) {
DEB << "SQL error: " << query.lastError().text();
DEB << "Statement: " << query.lastQuery();
lastError = query.lastError();
return {}; // return invalid Row
}
RowData_T prev_exp_data;
if(query.next()) {
auto r = query.record(); // retreive record
if (r.count() == 0) // row is empty
return {};
for (int i = 0; i < r.count(); i++){ // iterate through fields to get key:value map
if(!r.value(i).isNull()) {
prev_exp_data.insert(r.fieldName(i), r.value(i));
}
}
}
// add up the two query results
for(auto it = prev_exp_data.begin(); it != prev_exp_data.end(); it++) {
int prevXpValue = it.value().toInt();
int entryValue = entry_data.value(it.key()).toInt();
const QVariant sum = prevXpValue + entryValue;
entry_data.insert(it.key(), sum);
}
return entry_data;
}
QList Database::getForeignKeyConstraints(int foreign_row_id, OPL::DbTable table)
{
QString statement = QLatin1String("SELECT ROWID FROM flights WHERE ");
switch (table) {
case OPL::DbTable::Pilots:
statement.append(QLatin1String("pic=?"));
break;
case OPL::DbTable::Tails:
statement.append(QLatin1String("acft=?"));
break;
default:
DEB << "Not a valid target for this function.";
return QList();
break;
}
QSqlQuery query;
query.prepare(statement);
query.addBindValue(foreign_row_id);
query.exec();
if (!query.isActive()) {
lastError = query.lastError();
DEB << "Error";
DEB << statement;
DEB << query.lastError().text();
return QList();
}
QList row_ids;
while (query.next()) {
row_ids.append(query.value(0).toInt());
}
return row_ids;
}
QVector Database::customQuery(QString statement, int return_values)
{
QSqlQuery query(statement);
if(!query.exec()) {
lastError = query.lastError();
DEB << "Query Error: " << lastError.text();
return {};
}
if (!query.first()) {
LOG << "No result found. Check Query and Error.";
DEB << "Error: " << query.lastError().text();
DEB << "Statement: " << statement;
return QVector();
} else {
query.first();
query.previous();
QVector result;
while (query.next()) {
for (int i = 0; i < return_values ; i++) {
result.append(query.value(i));
}
}
lastError = QString();
return result;
}
}
QVector Database::getTable(OPL::DbTable table)
{
const QString query_str = QStringLiteral("SELECT * FROM ") + GLOBALS->getDbTableName(table);
QSqlQuery q;
q.prepare(query_str);
q.setForwardOnly(true);
if (!q.exec()) {
LOG << "SQL error: " << q.lastError().text();
LOG << "Statement: " << query_str;
lastError = q.lastError();
return {};
}
QVector entry_data;
while(q.next()) { // iterate through records
auto r = q.record();
//DEB << r;
RowData_T row;
for (int i = 0; i < r.count(); i++){
if(!r.value(i).isNull()) {
row.insert(r.fieldName(i), r.value(i));
}
}
entry_data.append(row);
}
return entry_data;
}
bool Database::createBackup(const QString& dest_file)
{
LOG << "Backing up current database to: " << dest_file;
Database::disconnect();
QFile db_file(QDir::toNativeSeparators(databaseFile.absoluteFilePath()));
if (!db_file.copy(QDir::toNativeSeparators(dest_file))) {
LOG << "Unable to backup old database:" << db_file.errorString();
return false;
}
LOG << "Backed up old database as:" << dest_file;
Database::connect();
emit connectionReset();
return true;
}
bool Database::restoreBackup(const QString& backup_file)
{
Database::disconnect();
LOG << "Restoring backup from file:" << backup_file;
QString databaseFilePath = QDir::toNativeSeparators(databaseFile.absoluteFilePath());
DEB << "DB File Path: " << databaseFilePath;
QString backupFilePath = QDir::toNativeSeparators(backup_file);
QFile dbFile(databaseFilePath);
if(dbFile.exists())
if(!dbFile.remove()) {
LOG << dbFile.errorString() << "Unable to remove current db file";
return false;
}
QFile backupFile(backupFilePath);
if(!backupFile.copy(databaseFilePath)) {
LOG << backupFile.errorString() << "Could not copy" << backupFile.fileName() << " to " << databaseFilePath;
return false;
}
LOG << "Backup successfully restored!";
Database::connect();
emit connectionReset();
return true;
}
bool Database::createSchema()
{
// Read Database layout from sql file
QFile f(OPL::Assets::DATABASE_SCHEMA);
f.open(QIODevice::ReadOnly);
QByteArray filedata = f.readAll();
// create individual queries for each table/view
auto list = filedata.split(';');
// make sure last empty line in sql file has not been parsed
if(list.last() == QByteArray("\n") || list.last() == QByteArray("\r\n"))
list.removeLast();
// Create Tables
QSqlQuery q;
QVector errors;
for (const auto &query_string : list) {
q.prepare(query_string);
if (!q.exec()) {
errors.append(q.lastError());
LOG << "Unable to execute query: ";
LOG << q.lastQuery();
LOG << q.lastError();
}
}
DB->updateLayout();
if (errors.isEmpty()) {
LOG << "Database succesfully created.";
return true;
} else {
LOG << "Database creation has failed. The following error(s) have ocurred: ";
for (const auto &error : std::as_const(errors)) {
LOG << error.type() << error.text();
}
return false;
}
}
bool Database::importTemplateData(bool use_local_ressources)
{
for (const auto& table : DB->getTemplateTables()) {
const QString table_name = OPL::GLOBALS->getDbTableName(table);
//clear table
QSqlQuery q;
q.prepare(QLatin1String("DELETE FROM ") + table_name);
if (!q.exec()) {
LOG << "Error clearing tables: " << q.lastError().text();
return false;
}
//Prepare data
QJsonArray data_to_commit;
QString error_message("Error importing data ");
if (use_local_ressources) {
data_to_commit = JsonHelper::readFileToDoc(QLatin1String(":database/templates/")
+ table_name + QLatin1String(".json")).array();
error_message.append(QLatin1String(" (ressource) "));
} else {
const QString file_path = OPL::Paths::filePath(OPL::Paths::Templates,
table_name + QLatin1String(".json"));
data_to_commit = JsonHelper::readFileToDoc(file_path).array();
//data_to_commit = AJson::readFileToDoc(AStandardPaths::directory(
// AStandardPaths::Templates).absoluteFilePath(
// table_name + QLatin1String(".json"))).array();
error_message.append(QLatin1String(" (downloaded) "));
}
// commit Data from Array
if (!DB->commit(data_to_commit, table)) {
LOG << error_message;
return false;
}
} // for table_name
return true;
}
bool Database::resetUserData()
{
QSqlQuery query;
for (const auto& table : DB->getUserTables()) {
query.prepare(QLatin1String("DELETE FROM ") + OPL::GLOBALS->getDbTableName(table));
if (!query.exec()) {
lastError = query.lastError();
return false;
}
}
return true;
}
} // namespace OPL