Commit 2e2ae1d5 authored by Linus Jahn's avatar Linus Jahn Committed by GitHub

Add database versioning and conversion (#110)

Closes #95.
parent 630b184f
......@@ -4,6 +4,7 @@ set(CURDIR ${CMAKE_CURRENT_LIST_DIR})
set(KAIDAN_SOURCES
${CURDIR}/main.cpp
${CURDIR}/Kaidan.cpp
${CURDIR}/Database.cpp
${CURDIR}/RosterController.cpp
${CURDIR}/RosterModel.cpp
${CURDIR}/MessageController.cpp
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017 LNJ <git@lnj.li>
*
* Kaidan 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.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Database.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
static const unsigned int DATABASE_LATEST_VERSION = 2;
static const char *DATABASE_TABLE_INFO = "dbinfo";
static const char *DATABASE_TABLE_MESSAGES = "Messages";
static const char *DATABASE_TABLE_ROSTER = "Roster";
Database::Database(QObject *parent) : QObject(parent)
{
version = -1;
database = QSqlDatabase::addDatabase("QSQLITE", "kaidan_default_db");
if (!database.isValid()) {
qFatal("Cannot add database: %s", qPrintable(database.lastError().text()));
}
}
Database::~Database()
{
database.close();
}
QSqlDatabase* Database::getDatabase()
{
return &database;
}
void Database::openDatabase()
{
const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (!writeDir.mkpath(".")) {
qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));
}
// Ensure that we have a writable location on all devices.
const QString fileName = writeDir.absoluteFilePath("messages.sqlite3");
// open() will create the SQLite database if it doesn't exist.
database.setDatabaseName(fileName);
if (!database.open()) {
qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
QFile::remove(fileName);
}
loadDatabaseInfo();
}
void Database::loadDatabaseInfo()
{
QStringList tables = database.tables();
if (!tables.contains(DATABASE_TABLE_INFO)) {
if (tables.contains(DATABASE_TABLE_MESSAGES) &&
tables.contains(DATABASE_TABLE_ROSTER)) {
// old Kaidan v0.1/v0.2 table
version = 1;
} else {
version = 0;
}
}
QSqlQuery query(database);
query.prepare("SELECT version FROM dbinfo");
if (!query.exec()) {
qWarning("Cannot query database info: %s", qPrintable(database.lastError().text()));
}
QSqlRecord record = query.record();
int versionCol = record.indexOf("version");
while (query.next()) {
version = query.value(versionCol).toInt();
}
}
bool Database::needToConvert()
{
if (version < DATABASE_LATEST_VERSION) {
return true;
}
return false;
}
void Database::convertDatabase()
{
qDebug() << "[Database] Converting database to latest version from verion" << version;
switch (version) {
case 0:
createNewDatabase();
break;
case 1:
convertDatabaseToV2();
// only break on last convertion step, to not enter default (!)
break;
default:
createNewDatabase();
}
QSqlQuery query(database);
query.prepare(QString("UPDATE dbinfo SET version = %1").arg(DATABASE_LATEST_VERSION));
if (!query.exec()) {
qDebug("Failed to query database: %s", qPrintable(query.lastError().text()));
}
database.commit();
version = DATABASE_LATEST_VERSION;
}
void Database::createNewDatabase()
{
QSqlQuery query(database);
//
// DB info
//
createDbInfoTable();
//
// Roster
//
if (!query.exec("CREATE TABLE IF NOT EXISTS 'Roster' ("
"'jid' TEXT NOT NULL,"
"'name' TEXT NOT NULL,"
"'lastExchanged' TEXT NOT NULL,"
"'unreadMessages' INTEGER,"
"'lastMessage' TEXT,"
"'lastOnline' TEXT," // < UNUSED v
"'activity' TEXT,"
"'status' TEXT,"
"'mood' TEXT" // < UNUSED ^
")"))
{
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
//
// Messages
//
if (!query.exec(
"CREATE TABLE IF NOT EXISTS 'Messages' ("
"'author' TEXT NOT NULL,"
"'author_resource' TEXT,"
"'recipient' TEXT NOT NULL,"
"'recipient_resource' TEXT,"
"'timestamp' TEXT NOT NULL,"
"'message' TEXT NOT NULL,"
"'id' TEXT NOT NULL,"
"'isSent' BOOL," // is sent to server
"'isDelivered' BOOL," // message has arrived at other client
"FOREIGN KEY('author') REFERENCES Roster ('jid'),"
"FOREIGN KEY('recipient') REFERENCES Roster ('jid')"
")"))
{
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
}
void Database::createDbInfoTable()
{
QSqlQuery query(database);
query.prepare("CREATE TABLE IF NOT EXISTS 'dbinfo' (version INTEGER NOT NULL)");
if (!query.exec()) {
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
query.prepare(QString("INSERT INTO 'dbinfo' (version) VALUES (%1)")
.arg(DATABASE_LATEST_VERSION));
if (!query.exec()) {
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
}
void Database::convertDatabaseToV2()
{
// create a new dbinfo table
createDbInfoTable();
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017 LNJ <git@lnj.li>
*
* Kaidan 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.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_H
#define DATABASE_H
#include <QObject>
#include <QSqlDatabase>
class Database : public QObject
{
Q_OBJECT
public:
Database(QObject *parent = 0);
~Database();
QSqlDatabase* getDatabase();
bool needToConvert();
void convertDatabase();
void openDatabase();
private:
void loadDatabaseInfo();
void createDbInfoTable();
void createNewDatabase();
void convertDatabaseToV2();
QSqlDatabase database;
int version;
};
#endif // DATABASE_H
......@@ -43,10 +43,21 @@
#include "VCardController.h"
#include "ServiceDiscoveryManager.h"
Kaidan::Kaidan(Swift::NetworkFactories* networkFactories, QObject *parent) : QObject(parent)
Kaidan::Kaidan(Swift::NetworkFactories *networkFactories, QObject *parent) :
QObject(parent)
{
netFactories = networkFactories;
connected = false;
netFactories = networkFactories;
database = new Database();
database->openDatabase();
if (database->needToConvert()) database->convertDatabase();
storages = new Swift::MemoryStorages(Swift::PlatformCryptoProvider::create());
messageController = new MessageController(database->getDatabase());
rosterController = new RosterController(database->getDatabase());
presenceController = new PresenceController();
vCardController = new VCardController();
serviceDiscoveryManager = new ServiceDiscoveryManager();
//
// Load settings data
......@@ -70,15 +81,6 @@ Kaidan::Kaidan(Swift::NetworkFactories* networkFactories, QObject *parent) : QOb
// create/update the full jid
updateFullJid();
// load controllers
messageController = new MessageController(&jid);
rosterController = new RosterController();
presenceController = new PresenceController();
vCardController = new VCardController();
serviceDiscoveryManager = new ServiceDiscoveryManager();
storages = new Swift::MemoryStorages(Swift::PlatformCryptoProvider::create());
}
Kaidan::~Kaidan()
......@@ -216,7 +218,7 @@ void Kaidan::setChatPartner(QString chatPartner)
this->chatPartner = chatPartner;
// upsate message controller
messageController->setChatPartner(&chatPartner);
messageController->setChatPartner(&chatPartner, &jid);
rosterController->setChatPartner(&chatPartner);
emit chatPartnerChanged();
......
......@@ -31,6 +31,7 @@
#include <Swiften/Client/Client.h>
#include <Swiften/Client/ClientXMLTracer.h>
// Kaidan
#include "Database.h"
#include "RosterController.h"
#include "MessageController.h"
#include "PresenceController.h"
......@@ -51,7 +52,7 @@ class Kaidan : public QObject
Q_PROPERTY(QString chatPartner READ getChatPartner WRITE setChatPartner NOTIFY chatPartnerChanged)
public:
Kaidan(Swift::NetworkFactories* networkFactories, QObject *parent = 0);
Kaidan(Swift::NetworkFactories *networkFactories, QObject *parent = 0);
~Kaidan();
Q_INVOKABLE void mainDisconnect();
......@@ -100,6 +101,7 @@ private:
Swift::NetworkFactories* netFactories;
Swift::MemoryStorages* storages;
Database* database;
RosterController* rosterController;
MessageController* messageController;
PresenceController* presenceController;
......
......@@ -38,10 +38,9 @@
#include "MessageModel.h"
#include "Notifications.h"
MessageController::MessageController(QString* ownJid_, QObject *parent) : QObject(parent)
MessageController::MessageController(QSqlDatabase *database, QObject *parent) : QObject(parent)
{
ownJid = ownJid_;
messageModel = new MessageModel();
messageModel = new MessageModel(database);
emit messageModelChanged();
}
......@@ -61,7 +60,7 @@ MessageModel* MessageController::getMessageModel()
return messageModel;
}
void MessageController::setChatPartner(QString *recipient)
void MessageController::setChatPartner(QString *recipient, QString* ownJid)
{
// we have to use ownJid here, because this should also be usable when
// we're offline or we haven't connected already.
......
......@@ -22,6 +22,7 @@
// Qt
#include <QObject>
#include <QSqlDatabase>
// Swiften
#include <Swiften/Client/Client.h>
#include <Swiften/Elements/Message.h>
......@@ -35,13 +36,13 @@ class MessageController : public QObject
Q_PROPERTY(MessageModel *messageModel READ getMessageModel NOTIFY messageModelChanged)
public:
MessageController(QString *ownJid_, QObject *parent = 0);
MessageController(QSqlDatabase *database, QObject *parent = 0);
~MessageController();
void setClient(Swift::Client *client_);
MessageModel* getMessageModel();
void setChatPartner(QString *recipient);
void setChatPartner(QString *recipient, QString* ownJid);
void sendMessage(QString *recipient_, QString *message_);
signals:
......
......@@ -30,38 +30,10 @@
static const char *conversationsTableName = "Messages";
static void createTable()
MessageModel::MessageModel(QSqlDatabase *database, QObject *parent) : QSqlTableModel(parent, *database)
{
if (QSqlDatabase::database().tables().contains(conversationsTableName)) {
// The table already exists; we don't need to do anything.
return;
}
QSqlQuery query;
if (!query.exec(
"CREATE TABLE IF NOT EXISTS 'Messages' ("
"'author' TEXT NOT NULL,"
"'author_resource' TEXT,"
"'recipient' TEXT NOT NULL,"
"'recipient_resource' TEXT,"
"'timestamp' TEXT NOT NULL,"
"'message' TEXT NOT NULL,"
"'id' TEXT NOT NULL,"
"'isSent' BOOL," // is sent to server
"'isDelivered' BOOL," // message has arrived at other client
"FOREIGN KEY('author') REFERENCES Roster ('jid'),"
"FOREIGN KEY('recipient') REFERENCES Roster ('jid')"
")"))
{
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
}
MessageModel::MessageModel(QObject *parent) : QSqlTableModel(parent)
{
createTable();
this->database = database;
setTable(conversationsTableName);
// sort in descending order of the timestamp column
setSort(4, Qt::DescendingOrder);
......@@ -99,14 +71,14 @@ QHash<int, QByteArray> MessageModel::roleNames() const
void MessageModel::setMessageAsSent(const QString msgId)
{
QSqlQuery newQuery;
QSqlQuery newQuery(*database);
newQuery.exec(QString("UPDATE 'Messages' SET 'isSent' = 1 WHERE id = '%1'").arg(msgId));
submitAll();
}
void MessageModel::setMessageAsDelivered(const QString msgId)
{
QSqlQuery newQuery;
QSqlQuery newQuery(*database);
newQuery.exec(QString("UPDATE 'Messages' SET 'isDelivered' = 1 WHERE id = '%1'").arg(msgId));
submitAll();
}
......
......@@ -27,7 +27,7 @@ class MessageModel : public QSqlTableModel
Q_OBJECT
public:
MessageModel(QObject *parent = 0);
MessageModel(QSqlDatabase *database, QObject *parent = 0);
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
......@@ -44,6 +44,7 @@ signals:
void recipientChanged();
private:
QSqlDatabase* database;
};
#endif // MESSAGEMODEL_H
......@@ -40,9 +40,10 @@
#include <boost/bind.hpp>
#include <boost/optional.hpp>
RosterController::RosterController(QObject *parent) : QObject(parent)
RosterController::RosterController(QSqlDatabase* database, QObject *parent) :
QObject(parent)
{
rosterModel = new RosterModel();
rosterModel = new RosterModel(database);
chatPartner = QString("");
}
......
......@@ -37,7 +37,7 @@ class RosterController : public QObject
Q_PROPERTY(RosterModel* rosterModel READ getRosterModel NOTIFY rosterModelChanged)
public:
RosterController(QObject *parent = 0);
RosterController(QSqlDatabase* database, QObject *parent = 0);
~RosterController();
void setClient(Swift::Client *client_);
......
......@@ -24,37 +24,16 @@
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QSqlDatabase>
#include <QSqlTableModel>
static const char *rosterTableName = "Roster";
static void createTable()
RosterModel::RosterModel(QSqlDatabase* database, QObject *parent) :
QSqlTableModel(parent, *database)
{
if (QSqlDatabase::database().tables().contains(QStringLiteral("Roster"))) {
// The table already exists; we don't need to do anything.
return;
}
QSqlQuery query;
if (!query.exec("CREATE TABLE IF NOT EXISTS 'Roster' ("
"'jid' TEXT NOT NULL,"
"'name' TEXT NOT NULL,"
"'lastExchanged' TEXT NOT NULL,"
"'unreadMessages' INTEGER,"
"'lastMessage' TEXT," // < UNUSED v
"'lastOnline' TEXT,"
"'activity' TEXT,"
"'status' TEXT,"
"'mood' TEXT" // < UNUSED ^
")"))
{
qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
}
}
this->database = database;
RosterModel::RosterModel(QObject *parent) : QSqlTableModel(parent)
{
createTable();
setTable(rosterTableName);
setEditStrategy(QSqlTableModel::OnManualSubmit);
......@@ -120,15 +99,19 @@ void RosterModel::insertContact(QString jid_, QString name_)
void RosterModel::removeContactByJid(QString jid_)
{
QSqlQuery newQuery;
newQuery.exec(QString("DELETE FROM 'Roster' WHERE jid = '%1'").arg(jid_));
QSqlQuery query(*database);
if (!query.exec(QString("DELETE FROM 'Roster' WHERE jid = '%1'").arg(jid_))) {
qDebug("Failed to query database: %s", qPrintable(query.lastError().text()));
}
submitAll();
}
void RosterModel::updateContactName(QString jid_, QString name_)
{
QSqlQuery newQuery;
newQuery.exec(QString("UPDATE 'Roster' SET name = '%1' WHERE jid = '%2'").arg(name_, jid_));
QSqlQuery query(*database);
if (!query.exec(QString("UPDATE 'Roster' SET name = '%1' WHERE jid = '%2'").arg(name_, jid_))) {
qDebug("Failed to query database: %s", qPrintable(query.lastError().text()));
}
submitAll();
}
......@@ -136,8 +119,10 @@ QStringList RosterModel::getJidList()
{
QStringList retVar;
QSqlQuery query;
query.exec("SELECT jid FROM Roster");
QSqlQuery query(*database);
if (!query.exec("SELECT jid FROM Roster")) {
qDebug("Failed to query database: %s", qPrintable(query.lastError().text()));
}
int jidCol = query.record().indexOf("jid");
......@@ -149,23 +134,26 @@ QStringList RosterModel::getJidList()
void RosterModel::removeListOfJids(QStringList* jidList)
{
QSqlQuery query;
QSqlQuery query(*database);
for (int i = 0; i < jidList->length(); i++) {
query.exec(QString("DELETE FROM 'Roster' WHERE jid = '%1'").arg(jidList->at(i)));
if (!query.exec(QString("DELETE FROM 'Roster' WHERE jid = '%1'")
.arg(jidList->at(i)))) {
qDebug("Failed to query database: %s", qPrintable(query.lastError().text()));
}
}
submitAll();
}
void RosterModel::setLastExchangedOfJid(QString *jid_, QString *date_)
{
QSqlQuery newQuery;
QSqlQuery newQuery(*database);
newQuery.exec(QString("UPDATE 'Roster' SET lastExchanged = '%1' WHERE jid = '%2'").arg(*date_, *jid_));
submitAll();
}
int RosterModel::getUnreadMessageCountOfJid(const QString* jid_)
{
QSqlQuery query;
QSqlQuery query(*database);
query.prepare(QString("SELECT unreadMessages FROM Roster WHERE jid = '%1'").arg(*jid_));
if (!query.exec()) {
......@@ -179,7 +167,7 @@ int RosterModel::getUnreadMessageCountOfJid(const QString* jid_)
void RosterModel::setUnreadMessageCountOfJid(const QString* jid_, const int count_)
{
QSqlQuery query;
QSqlQuery query(*database);
query.prepare(QString("UPDATE Roster SET unreadMessages = %1 WHERE jid = '%2'")
.arg(QString::number(count_), *jid_));
......@@ -196,7 +184,7 @@ void RosterModel::setUnreadMessageCountOfJid(const QString* jid_, const int coun
void RosterModel::setLastMessageForJid(QString *jid, QString *message)
{
QSqlQuery query;
QSqlQuery query(*database);
query.prepare(QString("UPDATE Roster SET lastMessage = %1 WHERE jid = '%2'")
.arg(*message, *jid));
......
......@@ -22,6 +22,7 @@
// Qt
#include <QObject>
#include <QSqlDatabase>
#include <QSqlTableModel>
#include <QQmlListProperty>
......@@ -29,7 +30,7 @@ class RosterModel : public QSqlTableModel
{
Q_OBJECT
public:
RosterModel(QObject *parent = 0);
RosterModel(QSqlDatabase *database, QObject *parent = 0);
QHash<int, QByteArray> roleNames() const;
QVariant data(const QModelIndex &index, int role) const;
......@@ -44,6 +45,9 @@ public:
int getUnreadMessageCountOfJid(const QString* jid_);