1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

refactor(History): Split peers table into chats and authors

peers had combined meaning, both being referenced by history for which
chat a message was in, and being reference by aliases for who authored a
message. This means that peers had conceptually different sub-groups:
all friends are both a chat and an author, but self is an author but not
a chat. With the addition of group chats this is amplified by groups
themselves being chats but not authors, and group members being authors
but not chats. Instead of having four sub-groups all within peers,
splitting peers into chats and authors gives a clean mapping,
simplifying interactions with the data.

In the new chats and authors tables, store what used to be a public_key
string as a BLOB, since it’s inherently a 32-byte binary value in both
cases. Call the public_key a UUID for chats, since group IDs are not
defined as public keys by toxcore.

Even though the data change is quite minor, the upgrade is large because
of SQLite's lack of support for modifying foreign key constrains for
existing tables. This means when peers are moved to new tables, all
tables referencing peers need to be cloned with a new foreign key
constraint, as well as all tables referencing those, recursively.
This commit is contained in:
Anthony Bilinski 2022-03-22 04:43:27 -07:00
parent f6dddf7c58
commit 16ac8a8eac
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
14 changed files with 953 additions and 172 deletions

View File

@ -317,6 +317,8 @@ set(${PROJECT_NAME}_SOURCES
src/persistence/db/rawdatabase.h
src/persistence/db/upgrades/dbupgrader.cpp
src/persistence/db/upgrades/dbupgrader.h
src/persistence/db/upgrades/dbto11.h
src/persistence/db/upgrades/dbto11.cpp
src/persistence/history.cpp
src/persistence/history.h
src/persistence/ifriendsettings.cpp

View File

@ -48,6 +48,7 @@ auto_test(net bsu "${${PROJECT_NAME}_RESOURCES}" "") # needs nodes list
auto_test(chatlog chatlinestorage "" "")
auto_test(persistence paths "" "")
auto_test(persistence dbschema "" "dbutility_library")
auto_test(persistence/dbupgrade dbTo11 "" "dbutility_library")
auto_test(persistence offlinemsgengine "" "")
if(NOT "${SMILEYS}" STREQUAL "DISABLED")
auto_test(persistence smileypack "${SMILEY_RESOURCES}" "") # needs emojione

View File

@ -363,15 +363,13 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
auto currentIdx = nextIdx++;
switch (message.content.getType()) {
case HistMessageContentType::file: {
auto sender = ToxPk(message.sender);
const auto date = message.timestamp;
const auto file = message.content.asFile();
const auto chatLogFile = ChatLogFile{date, file};
sessionChatLog.insertFileAtIdx(currentIdx, sender, message.dispName, chatLogFile);
sessionChatLog.insertFileAtIdx(currentIdx, message.sender, message.dispName, chatLogFile);
break;
}
case HistMessageContentType::message: {
auto sender = ToxPk(message.sender);
auto messageContent = message.content.asMessage();
auto isAction = handleActionPrefix(messageContent);
@ -394,15 +392,15 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
auto chatLogMessage = ChatLogMessage{message.state, processedMessage};
switch (message.state) {
case MessageState::complete:
sessionChatLog.insertCompleteMessageAtIdx(currentIdx, sender, message.dispName,
sessionChatLog.insertCompleteMessageAtIdx(currentIdx, message.sender, message.dispName,
chatLogMessage);
break;
case MessageState::pending:
sessionChatLog.insertIncompleteMessageAtIdx(currentIdx, sender, message.dispName,
sessionChatLog.insertIncompleteMessageAtIdx(currentIdx, message.sender, message.dispName,
chatLogMessage, dispatchedMessageIt.key());
break;
case MessageState::broken:
sessionChatLog.insertBrokenMessageAtIdx(currentIdx, sender, message.dispName,
sessionChatLog.insertBrokenMessageAtIdx(currentIdx, message.sender, message.dispName,
chatLogMessage);
break;
}

View File

@ -74,6 +74,13 @@ public:
, rowCallback{rowCallback_}
{
}
Query(QString query_, QVector<QByteArray> blobs_,
const std::function<void(const QVector<QVariant>&)>& rowCallback_)
: query{query_.toUtf8()}
, blobs{blobs_}
, rowCallback{rowCallback_}
{
}
Query() = default;
private:

View File

@ -0,0 +1,294 @@
/*
Copyright © 2022 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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.
qTox 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 qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "dbto11.h"
#include "src/core/toxpk.h"
#include <QDebug>
bool DbTo11::dbSchema10to11(RawDatabase& db)
{
// splitting peers table into separate chat and authors table.
// peers had a dual-meaning of each friend having their own chat, but also each peer
// being authors of messages. This wasn't true for self which was in the peers table
// but had no related chat. For upcomming group history, the confusion is amplified
// since each group is a chat so is in peers, but authors no messages, and each group
// member is an author so is in peers, but has no chat.
// Splitting peers makes the relation much clearer and the tables have a single meaning.
QVector<RawDatabase::Query> upgradeQueries;
if (!PeersToAuthors::appendPeersToAuthorsQueries(db, upgradeQueries)) {
return false;
}
if (!PeersToChats::appendPeersToChatsQueries(db, upgradeQueries)) {
return false;
}
appendDropPeersQueries(upgradeQueries);
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 11;"));
bool transactionPass = db.execNow(upgradeQueries);
if (transactionPass) {
return db.execNow("VACUUM");
}
return transactionPass;
}
bool DbTo11::PeersToAuthors::appendPeersToAuthorsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries)
{
appendCreateNewTablesQueries(upgradeQueries);
if (!appendPopulateAuthorQueries(db, upgradeQueries)) {
return false;
}
if (!appendUpdateAliasesFkQueries(db, upgradeQueries)) {
return false;
}
appendUpdateTextMessagesFkQueries(upgradeQueries);
appendUpdateFileTransfersFkQueries(upgradeQueries);
appendReplaceOldTablesQueries(upgradeQueries);
return true;
}
void DbTo11::PeersToAuthors::appendCreateNewTablesQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE authors (id INTEGER PRIMARY KEY, "
"public_key BLOB NOT NULL UNIQUE);"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE aliases_new ("
"id INTEGER PRIMARY KEY, "
"owner INTEGER, "
"display_name BLOB NOT NULL, "
"UNIQUE(owner, display_name), "
"FOREIGN KEY (owner) REFERENCES authors(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE text_messages_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "
"sender_alias INTEGER NOT NULL, "
"message BLOB NOT NULL, "
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
"FOREIGN KEY (sender_alias) REFERENCES aliases_new(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE file_transfers_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'F'), "
"sender_alias INTEGER NOT NULL, "
"file_restart_id BLOB NOT NULL, "
"file_name BLOB NOT NULL, "
"file_path BLOB NOT NULL, "
"file_hash BLOB NOT NULL, "
"file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL, "
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
"FOREIGN KEY (sender_alias) REFERENCES aliases_new(id));"));
}
bool DbTo11::PeersToAuthors::appendPopulateAuthorQueries(RawDatabase &db, QVector<RawDatabase::Query> &upgradeQueries)
{
// don't copy over directly, so that we can convert from string to blob
return db.execNow(RawDatabase::Query(QStringLiteral(
"SELECT DISTINCT peers.public_key FROM peers JOIN aliases ON peers.id=aliases.owner"),
[&](const QVector<QVariant>& row) {
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO authors (public_key) VALUES (?)"),
{ToxPk{row[0].toString()}.getByteArray()});
}
));
}
bool DbTo11::PeersToAuthors::appendUpdateAliasesFkQueries(RawDatabase &db, QVector<RawDatabase::Query>& upgradeQueries)
{
return db.execNow(RawDatabase::Query(QStringLiteral(
"SELECT id, public_key FROM peers"),
[&](const QVector<QVariant>& row) {
const auto oldPeerId = row[0].toLongLong();
const auto pk = ToxPk{row[1].toString()};
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO aliases_new (id, owner, display_name) "
"SELECT id, "
"(SELECT id FROM authors WHERE public_key = ?), "
"display_name "
"FROM aliases WHERE "
"owner = '%1';").arg(oldPeerId),
{pk.getByteArray()});
}));
}
void DbTo11::PeersToAuthors::appendReplaceOldTablesQueries(QVector<RawDatabase::Query>& upgradeQueries) {
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE text_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE text_messages_new RENAME TO text_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE file_transfers;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE file_transfers_new RENAME TO file_transfers;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE aliases;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE aliases_new RENAME TO aliases;"));
}
bool DbTo11::PeersToChats::appendPeersToChatsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries)
{
appendCreateNewTablesQueries(upgradeQueries);
if (!appendPopulateChatsQueries(db, upgradeQueries)) {
return false;
}
if (!appendUpdateHistoryFkQueries(db, upgradeQueries)) {
return false;
}
appendUpdateTextMessagesFkQueries(upgradeQueries);
appendUpdateFileTransfersFkQueries(upgradeQueries);
appendUpdateSystemMessagesFkQueries(upgradeQueries);
appendUpdateFauxOfflinePendingFkQueries(upgradeQueries);
appendUpdateBrokenMessagesFkQueries(upgradeQueries);
appendReplaceOldTablesQueries(upgradeQueries);
return true;
}
void DbTo11::PeersToChats::appendCreateNewTablesQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE chats (id INTEGER PRIMARY KEY, "
"uuid BLOB NOT NULL UNIQUE);"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE history_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL DEFAULT 'T' CHECK (message_type in ('T','F','S')), "
"timestamp INTEGER NOT NULL, "
"chat_id INTEGER NOT NULL, "
"UNIQUE (id, message_type), "
"FOREIGN KEY (chat_id) REFERENCES chats(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE text_messages_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "
"sender_alias INTEGER NOT NULL, "
"message BLOB NOT NULL, "
"FOREIGN KEY (id, message_type) REFERENCES history_new(id, message_type), "
"FOREIGN KEY (sender_alias) REFERENCES aliases(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE file_transfers_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'F'), "
"sender_alias INTEGER NOT NULL, "
"file_restart_id BLOB NOT NULL, "
"file_name BLOB NOT NULL, "
"file_path BLOB NOT NULL, "
"file_hash BLOB NOT NULL, "
"file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL, "
"FOREIGN KEY (id, message_type) REFERENCES history_new(id, message_type), "
"FOREIGN KEY (sender_alias) REFERENCES aliases(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE system_messages_new "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'S'), "
"system_message_type INTEGER NOT NULL, "
"arg1 BLOB, "
"arg2 BLOB, "
"arg3 BLOB, "
"arg4 BLOB, "
"FOREIGN KEY (id, message_type) REFERENCES history_new(id, message_type));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE faux_offline_pending_new (id INTEGER PRIMARY KEY, "
"required_extensions INTEGER NOT NULL DEFAULT 0, "
"FOREIGN KEY (id) REFERENCES history_new(id));"));
upgradeQueries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE broken_messages_new (id INTEGER PRIMARY KEY, "
"reason INTEGER NOT NULL DEFAULT 0, "
"FOREIGN KEY (id) REFERENCES history_new(id));"));
}
bool DbTo11::PeersToChats::appendPopulateChatsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries)
{
// don't copy over directly, so that we can convert from string to blob
return db.execNow(RawDatabase::Query(QStringLiteral(
"SELECT DISTINCT peers.public_key FROM peers JOIN history ON peers.id=history.chat_id"),
[&](const QVector<QVariant>& row) {
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO chats (uuid) VALUES (?)"),
{ToxPk{row[0].toString()}.getByteArray()});
}
));
}
bool DbTo11::PeersToChats::appendUpdateHistoryFkQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries)
{
return db.execNow(RawDatabase::Query(QStringLiteral(
"SELECT DISTINCT peers.id, peers.public_key FROM peers JOIN history ON peers.id=history.chat_id"),
[&](const QVector<QVariant>& row) {
const auto oldPeerId = row[0].toLongLong();
const auto pk = ToxPk{row[1].toString()};
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO history_new (id, message_type, timestamp, chat_id) "
"SELECT id, "
"message_type, "
"timestamp, "
"(SELECT id FROM chats WHERE uuid = ?) "
"FROM history WHERE chat_id = '%1'").arg(oldPeerId),
{pk.getByteArray()});
}));
}
void DbTo11::PeersToChats::appendReplaceOldTablesQueries(QVector<RawDatabase::Query>& upgradeQueries) {
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE text_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE text_messages_new RENAME TO text_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE file_transfers;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE file_transfers_new RENAME TO file_transfers;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE system_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE system_messages_new RENAME TO system_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE faux_offline_pending;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE faux_offline_pending_new RENAME TO faux_offline_pending;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE broken_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE broken_messages_new RENAME TO broken_messages;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE history;"));
upgradeQueries += RawDatabase::Query(QStringLiteral("ALTER TABLE history_new RENAME TO history;"));
}
void DbTo11::appendUpdateTextMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO text_messages_new SELECT * FROM text_messages"));
}
void DbTo11::appendUpdateFileTransfersFkQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO file_transfers_new SELECT * FROM file_transfers"));
}
void DbTo11::PeersToChats::appendUpdateSystemMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO system_messages_new SELECT * FROM system_messages"));
}
void DbTo11::PeersToChats::appendUpdateFauxOfflinePendingFkQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO faux_offline_pending_new SELECT * FROM faux_offline_pending"));
}
void DbTo11::PeersToChats::appendUpdateBrokenMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral(
"INSERT INTO broken_messages_new SELECT * FROM broken_messages"));
}
void DbTo11::appendDropPeersQueries(QVector<RawDatabase::Query>& upgradeQueries)
{
upgradeQueries += RawDatabase::Query(QStringLiteral("DROP TABLE peers;"));
}

View File

@ -0,0 +1,55 @@
/*
Copyright © 2022 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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.
qTox 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 qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "src/persistence/db/rawdatabase.h"
#include <QVector>
class ToxPk;
namespace DbTo11
{
bool dbSchema10to11(RawDatabase& db);
namespace PeersToAuthors
{
bool appendPeersToAuthorsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
void appendCreateNewTablesQueries(QVector<RawDatabase::Query>& upgradeQueries);
bool appendPopulateAuthorQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
bool appendUpdateAliasesFkQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
void appendReplaceOldTablesQueries(QVector<RawDatabase::Query>& upgradeQueries);
} // namespace PeersToAuthors
namespace PeersToChats
{
bool appendPeersToChatsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
void appendCreateNewTablesQueries(QVector<RawDatabase::Query>& upgradeQueries);
bool appendPopulateChatsQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
bool appendUpdateHistoryFkQueries(RawDatabase& db, QVector<RawDatabase::Query>& upgradeQueries);
void appendReplaceOldTablesQueries(QVector<RawDatabase::Query>& upgradeQueries);
void appendUpdateSystemMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries);
void appendUpdateBrokenMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries);
void appendUpdateFauxOfflinePendingFkQueries(QVector<RawDatabase::Query>& upgradeQueries);
} // namespace PeersToChats
void appendUpdateTextMessagesFkQueries(QVector<RawDatabase::Query>& upgradeQueries);
void appendUpdateFileTransfersFkQueries(QVector<RawDatabase::Query>& upgradeQueries);
void appendDropPeersQueries(QVector<RawDatabase::Query>& upgradeQueries);
} // namespace DbTo11

View File

@ -21,12 +21,13 @@
#include "src/core/chatid.h"
#include "src/core/toxpk.h"
#include "src/persistence/db/rawdatabase.h"
#include "src/persistence/db/upgrades/dbto11.h"
#include <QDebug>
#include <QString>
namespace {
constexpr int SCHEMA_VERSION = 10;
constexpr int SCHEMA_VERSION = 11;
struct BadEntry
{
@ -263,7 +264,7 @@ bool DbUpgrader::dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
dbSchema3to4, dbSchema4to5, dbSchema5to6,
dbSchema6to7, dbSchema7to8, dbSchema8to9,
dbSchema9to10};
dbSchema9to10, DbTo11::dbSchema10to11};
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
assert(upgradeFns.size() == SCHEMA_VERSION);
@ -286,13 +287,15 @@ bool DbUpgrader::createCurrentSchema(RawDatabase& db)
{
QVector<RawDatabase::Query> queries;
queries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE peers (id INTEGER PRIMARY KEY, "
"public_key TEXT NOT NULL UNIQUE);"
"CREATE TABLE authors (id INTEGER PRIMARY KEY, "
"public_key BLOB NOT NULL UNIQUE);"
"CREATE TABLE chats (id INTEGER PRIMARY KEY, "
"uuid BLOB NOT NULL UNIQUE);"
"CREATE TABLE aliases (id INTEGER PRIMARY KEY, "
"owner INTEGER, "
"display_name BLOB NOT NULL, "
"UNIQUE(owner, display_name), "
"FOREIGN KEY (owner) REFERENCES peers(id));"
"FOREIGN KEY (owner) REFERENCES authors(id));"
"CREATE TABLE history "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL DEFAULT 'T' CHECK (message_type in ('T','F','S')), "
@ -301,7 +304,7 @@ bool DbUpgrader::createCurrentSchema(RawDatabase& db)
// Message subtypes want to reference the following as a foreign key. Foreign keys must be
// guaranteed to be unique. Since an ID is already unique, id + message type is also unique
"UNIQUE (id, message_type), "
"FOREIGN KEY (chat_id) REFERENCES peers(id)); "
"FOREIGN KEY (chat_id) REFERENCES chats(id)); "
"CREATE TABLE text_messages "
"(id INTEGER PRIMARY KEY, "
"message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "

View File

@ -38,4 +38,5 @@ namespace DbUpgrader
bool dbSchema7to8(RawDatabase& db);
bool dbSchema8to9(RawDatabase& db);
bool dbSchema9to10(RawDatabase& db);
// 10to11 from DbTo11::dbSchema10to11
}

View File

@ -43,32 +43,51 @@ MessageState getMessageState(bool isPending, bool isBroken)
return messageState;
}
QString generatePeerIdString(ToxPk const& pk)
void addAuthorIdSubQuery(QString& queryString, QVector<QByteArray>& boundParams, const ToxPk& authorPk)
{
return QString("(SELECT id FROM peers WHERE public_key = '%1')").arg(pk.toString());
boundParams.append(authorPk.getByteArray());
queryString += "(SELECT id FROM authors WHERE public_key = ?)";
}
RawDatabase::Query generateEnsurePkInPeers(ToxPk const& pk)
void addChatIdSubQuery(QString& queryString, QVector<QByteArray>& boundParams, const ChatId& chatId)
{
return RawDatabase::Query{QStringLiteral("INSERT OR IGNORE INTO peers (public_key) "
"VALUES ('%1')").arg(pk.toString())};
boundParams.append(chatId.getByteArray());
queryString += "(SELECT id FROM chats WHERE uuid = ?)";
}
RawDatabase::Query generateEnsurePkInChats(ToxPk const& pk)
{
return RawDatabase::Query{QStringLiteral("INSERT OR IGNORE INTO chats (uuid) "
"VALUES (?)"), {pk.getByteArray()}};
}
RawDatabase::Query generateEnsurePkInAuthors(ToxPk const& pk)
{
return RawDatabase::Query{QStringLiteral("INSERT OR IGNORE INTO authors (public_key) "
"VALUES (?)"), {pk.getByteArray()}};
}
RawDatabase::Query generateUpdateAlias(ToxPk const& pk, QString const& dispName)
{
return RawDatabase::Query(
QString("INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (%1, ?);").arg(generatePeerIdString(pk)),
{dispName.toUtf8()});
QVector<QByteArray> boundParams;
QString queryString = QStringLiteral(
"INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (");
addAuthorIdSubQuery(queryString, boundParams, pk);
queryString += ", ?);";
boundParams += dispName.toUtf8();
return RawDatabase::Query{queryString, boundParams};
}
RawDatabase::Query generateHistoryTableInsertion(char type, const QDateTime& time, const ToxPk& friendPk)
{
return RawDatabase::Query(QString("INSERT INTO history (message_type, timestamp, chat_id) "
"VALUES ('%1', %2, %3);")
.arg(type)
.arg(time.toMSecsSinceEpoch())
.arg(generatePeerIdString(friendPk)));
QVector<QByteArray> boundParams;
QString queryString = QStringLiteral("INSERT INTO history (message_type, timestamp, chat_id) "
"VALUES ('%1', %2, ")
.arg(type)
.arg(time.toMSecsSinceEpoch());
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += ");";
return RawDatabase::Query(queryString, boundParams);
}
/**
@ -88,21 +107,25 @@ generateNewTextMessageQueries(const ToxPk& friendPk, const QString& message, con
{
QVector<RawDatabase::Query> queries;
queries += generateEnsurePkInPeers(friendPk);
queries += generateEnsurePkInPeers(sender);
queries += generateEnsurePkInChats(friendPk);
queries += generateEnsurePkInAuthors(sender);
queries += generateUpdateAlias(sender, dispName);
queries += generateHistoryTableInsertion('T', time, friendPk);
queries += RawDatabase::Query(
QString("INSERT INTO text_messages (id, message_type, sender_alias, message) "
QVector<QByteArray> boundParams;
QString queryString = QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) "
"VALUES ( "
" last_insert_rowid(), "
" 'T', "
" (SELECT id FROM aliases WHERE owner=%1 and display_name=?), "
" ?"
");")
.arg(generatePeerIdString(sender)),
{dispName.toUtf8(), message.toUtf8()}, insertIdCallback);
" (SELECT id FROM aliases WHERE owner=");
addAuthorIdSubQuery(queryString, boundParams, sender);
queryString += " and display_name=?";
boundParams += dispName.toUtf8();
queryString += "), ?";
boundParams += message.toUtf8();
queryString += ");";
queries += RawDatabase::Query(queryString, boundParams, insertIdCallback);
if (!isDelivered) {
queries += RawDatabase::Query{
@ -120,7 +143,7 @@ QVector<RawDatabase::Query> generateNewSystemMessageQueries(const ToxPk& friendP
{
QVector<RawDatabase::Query> queries;
queries += generateEnsurePkInPeers(friendPk);
queries += generateEnsurePkInChats(friendPk);
queries += generateHistoryTableInsertion('S', systemMessage.timestamp, friendPk);
QVector<QByteArray> blobs;
@ -230,8 +253,9 @@ void History::eraseHistory()
"DELETE FROM file_transfers;"
"DELETE FROM system_messages;"
"DELETE FROM history;"
"DELETE FROM chats;"
"DELETE FROM aliases;"
"DELETE FROM peers;"
"DELETE FROM authors;"
"VACUUM;");
}
@ -245,37 +269,59 @@ void History::removeFriendHistory(const ToxPk& friendPk)
return;
}
QString queryText = QString("DELETE FROM faux_offline_pending "
QVector<QByteArray> boundParams;
QString queryString = QStringLiteral("DELETE FROM faux_offline_pending "
"WHERE faux_offline_pending.id IN ( "
" SELECT faux_offline_pending.id FROM faux_offline_pending "
" LEFT JOIN history ON faux_offline_pending.id = history.id "
" WHERE chat_id=%1 "
" WHERE chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(
"); "
"DELETE FROM broken_messages "
"WHERE broken_messages.id IN ( "
" SELECT broken_messages.id FROM broken_messages "
" LEFT JOIN history ON broken_messages.id = history.id "
" WHERE chat_id=%1 "
" WHERE chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(
"); "
"DELETE FROM text_messages "
"WHERE id IN ("
" SELECT id from history "
" WHERE message_type = 'T' AND chat_id=%1);"
" WHERE message_type = 'T' AND chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(
");"
"DELETE FROM file_transfers "
"WHERE id IN ( "
" SELECT id from history "
" WHERE message_type = 'F' AND chat_id=%1);"
" WHERE message_type = 'F' AND chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(
");"
"DELETE FROM system_messages "
"WHERE id IN ( "
" SELECT id from history "
" WHERE message_type = 'S' AND chat_id=%1);"
"DELETE FROM history WHERE chat_id=%1; "
"DELETE FROM aliases WHERE owner=%1; "
"DELETE FROM peers WHERE id=%1; "
"VACUUM;")
.arg(generatePeerIdString(friendPk));
" WHERE message_type = 'S' AND chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(");"
"DELETE FROM history WHERE chat_id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral("; "
"DELETE FROM chats WHERE id=");
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral("; "
"DELETE FROM aliases WHERE owner=");
addAuthorIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral("; "
"DELETE FROM authors WHERE id=");
addAuthorIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral("; "
"VACUUM;");
if (!db->execNow(queryText)) {
RawDatabase::Query query = {queryString, boundParams};
if (!db->execNow(query)) {
qWarning() << "Failed to remove friend's history";
}
}
@ -300,16 +346,16 @@ History::generateNewFileTransferQueries(const ToxPk& friendPk, const ToxPk& send
{
QVector<RawDatabase::Query> queries;
queries += generateEnsurePkInPeers(friendPk);
queries += generateEnsurePkInPeers(sender);
queries += generateEnsurePkInChats(friendPk);
queries += generateEnsurePkInAuthors(sender);
queries += generateUpdateAlias(sender, dispName);
queries += generateHistoryTableInsertion('F', time, friendPk);
std::weak_ptr<History> weakThis = shared_from_this();
auto fileId = insertionData.fileId;
queries +=
RawDatabase::Query(QString(
QString queryString;
queryString += QStringLiteral(
"INSERT INTO file_transfers "
" (id, message_type, sender_alias, "
" file_restart_id, file_name, file_path, "
@ -317,28 +363,29 @@ History::generateNewFileTransferQueries(const ToxPk& friendPk, const ToxPk& send
"VALUES ( "
" last_insert_rowid(), "
" 'F', "
" (SELECT id FROM aliases WHERE owner=%1 AND display_name=?), "
" ?, "
" ?, "
" ?, "
" ?, "
" %2, "
" %3, "
" %4 "
");")
.arg(generatePeerIdString(sender))
.arg(insertionData.size)
.arg(insertionData.direction)
.arg(ToxFile::CANCELED),
{dispName.toUtf8(), insertionData.fileId,
insertionData.fileName.toUtf8(), insertionData.filePath.toUtf8(),
QByteArray()},
" (SELECT id FROM aliases WHERE owner=");
QVector<QByteArray> boundParams;
addAuthorIdSubQuery(queryString, boundParams, sender);
queryString += " AND display_name=?";
boundParams += dispName.toUtf8();
queryString += "), ?";
boundParams += insertionData.fileId;
queryString += ", ?";
boundParams += insertionData.fileName.toUtf8();
queryString += ", ?";
boundParams += insertionData.filePath.toUtf8();
queryString += ", ?";
boundParams += QByteArray();
queryString += QStringLiteral(", %1, %2, %3);")
.arg(insertionData.size)
.arg(insertionData.direction)
.arg(ToxFile::CANCELED);
queries += RawDatabase::Query(queryString, boundParams,
[weakThis, fileId](RowId id) {
auto pThis = weakThis.lock();
if (pThis)
emit pThis->fileInserted(id, fileId);
});
return queries;
}
@ -472,9 +519,8 @@ size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const Q
QString queryText = QString("SELECT COUNT(history.id) "
"FROM history "
"JOIN peers chat ON chat_id = chat.id "
"WHERE chat.public_key='%1'")
.arg(friendPk.toString());
"JOIN chats ON chat_id = chats.id "
"WHERE chats.uuid = ?");
if (date.isNull()) {
queryText += ";";
@ -487,7 +533,7 @@ size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const Q
numMessages = row[0].toLongLong();
};
db->execNow({queryText, rowCallback});
db->execNow({queryText, {friendPk.getByteArray()}, rowCallback});
return numMessages;
}
@ -501,30 +547,6 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
QList<HistMessage> messages;
// Don't forget to update the rowCallback if you change the selected columns!
QString queryText =
QString(
"SELECT history.id, history.message_type, history.timestamp, faux_offline_pending.id, "
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
" file_restart_id, file_name, file_path, file_size, file_transfers.direction, "
" file_state, peers.public_key as sender_key, aliases.display_name, "
" system_messages.system_message_type, system_messages.arg1, system_messages.arg2, "
" system_messages.arg3, system_messages.arg4 "
"FROM history "
"LEFT JOIN text_messages ON history.id = text_messages.id "
"LEFT JOIN file_transfers ON history.id = file_transfers.id "
"LEFT JOIN system_messages ON system_messages.id == history.id "
"LEFT JOIN aliases ON text_messages.sender_alias = aliases.id OR "
"file_transfers.sender_alias = aliases.id "
"LEFT JOIN peers ON aliases.owner = peers.id "
"LEFT JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
"LEFT JOIN broken_messages ON broken_messages.id = history.id "
"WHERE history.chat_id = %1 "
"LIMIT %2 OFFSET %3;")
.arg(generatePeerIdString(friendPk))
.arg(lastIdx - firstIdx)
.arg(firstIdx);
auto rowCallback = [&friendPk, &messages](const QVector<QVariant>& row) {
// If the select statement is changed please update these constants
constexpr auto messageOffset = 6;
@ -552,10 +574,10 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
assert(!it->isNull());
const auto messageContent = (*it++).toString();
it = std::next(row.begin(), senderOffset);
const auto senderKey = (*it++).toString();
const auto senderKey = ToxPk{(*it++).toByteArray()};
const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
messages += HistMessage(id, messageState, requiredExtensions, timestamp,
friendPk.toString(), senderName, senderKey, messageContent);
friendPk, senderName, senderKey, messageContent);
break;
}
case 'F': {
@ -575,9 +597,9 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
file.status = status;
it = std::next(row.begin(), senderOffset);
const auto senderKey = (*it++).toString();
const auto senderKey = ToxPk{(*it++).toByteArray()};
const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
messages += HistMessage(id, messageState, timestamp, friendPk.toString(), senderName,
messages += HistMessage(id, messageState, timestamp, friendPk, senderName,
senderKey, file);
break;
}
@ -594,12 +616,37 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
});
it = argEnd;
messages += HistMessage(id, timestamp, friendPk.toString(), systemMessage);
messages += HistMessage(id, timestamp, friendPk, systemMessage);
break;
}
};
db->execNow({queryText, rowCallback});
// Don't forget to update the rowCallback if you change the selected columns!
QString queryString =
QStringLiteral(
"SELECT history.id, history.message_type, history.timestamp, faux_offline_pending.id, "
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
" file_restart_id, file_name, file_path, file_size, file_transfers.direction, "
" file_state, authors.public_key as sender_key, aliases.display_name, "
" system_messages.system_message_type, system_messages.arg1, system_messages.arg2, "
" system_messages.arg3, system_messages.arg4 "
"FROM history "
"LEFT JOIN text_messages ON history.id = text_messages.id "
"LEFT JOIN file_transfers ON history.id = file_transfers.id "
"LEFT JOIN system_messages ON system_messages.id == history.id "
"LEFT JOIN aliases ON text_messages.sender_alias = aliases.id OR "
"file_transfers.sender_alias = aliases.id "
"LEFT JOIN authors ON aliases.owner = authors.id "
"LEFT JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
"LEFT JOIN broken_messages ON broken_messages.id = history.id "
"WHERE history.chat_id = ");
QVector<QByteArray> boundParams;
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(
" LIMIT %1 OFFSET %2;")
.arg(lastIdx - firstIdx)
.arg(firstIdx);
db->execNow({queryString, boundParams, rowCallback});
return messages;
}
@ -610,20 +657,6 @@ QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk
return {};
}
auto queryText =
QString(
"SELECT history.id, history.timestamp, faux_offline_pending.id, "
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
" peers.public_key as sender_key, aliases.display_name "
"FROM history "
"JOIN text_messages ON history.id = text_messages.id "
"JOIN aliases ON text_messages.sender_alias = aliases.id "
"JOIN peers ON aliases.owner = peers.id "
"JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
"LEFT JOIN broken_messages ON broken_messages.id = history.id "
"WHERE history.chat_id = %1 AND history.message_type = 'T';")
.arg(generatePeerIdString(friendPk));
QList<History::HistMessage> ret;
auto rowCallback = [&friendPk, &ret](const QVector<QVariant>& row) {
auto it = row.begin();
@ -635,16 +668,31 @@ QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk
auto extensionSet = ExtensionSet((*it++).toLongLong());
auto isBroken = !(*it++).isNull();
auto messageContent = (*it++).toString();
auto senderKey = (*it++).toString();
auto senderKey = ToxPk{(*it++).toByteArray()};
auto displayName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
MessageState messageState = getMessageState(isPending, isBroken);
ret += {id, messageState, extensionSet, timestamp, friendPk.toString(),
ret += {id, messageState, extensionSet, timestamp, friendPk,
displayName, senderKey, messageContent};
};
db->execNow({queryText, rowCallback});
QString queryString =
QStringLiteral(
"SELECT history.id, history.timestamp, faux_offline_pending.id, "
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
" authors.public_key as sender_key, aliases.display_name "
"FROM history "
"JOIN text_messages ON history.id = text_messages.id "
"JOIN aliases ON text_messages.sender_alias = aliases.id "
"JOIN authors ON aliases.owner = authors.id "
"JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
"LEFT JOIN broken_messages ON broken_messages.id = history.id "
"WHERE history.chat_id = ");
QVector<QByteArray> boundParams;
addChatIdSubQuery(queryString, boundParams, friendPk);
queryString += QStringLiteral(" AND history.message_type = 'T';");
db->execNow({queryString, boundParams, rowCallback});
return ret;
}
@ -730,19 +778,20 @@ QDateTime History::getDateWhereFindPhrase(const ToxPk& friendPk, const QDateTime
break;
}
QString queryText =
auto query = RawDatabase::Query(
QStringLiteral("SELECT timestamp "
"FROM history "
"JOIN peers chat ON chat_id = chat.id "
"JOIN chats ON chat_id = chats.id "
"JOIN text_messages ON history.id = text_messages.id "
"WHERE chat.public_key='%1' "
"AND %2 "
"%3")
.arg(friendPk.toString())
"WHERE chats.uuid = ? "
"AND %1 "
"%2")
.arg(message)
.arg(period);
.arg(period),
{friendPk.getByteArray()},
rowCallback);
db->execNow({queryText, rowCallback});
db->execNow(query);
return result;
}
@ -767,39 +816,6 @@ QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(con
return {};
}
auto friendPkString = friendPk.toString();
// No guarantee that this is the most efficient way to do this...
// We want to count messages that happened for a friend before a
// certain date. We do this by re-joining our table a second time
// but this time with the only filter being that our id is less than
// the ID of the corresponding row in the table that is grouped by day
auto countMessagesForFriend =
QString("SELECT COUNT(*) - 1 " // Count - 1 corresponds to 0 indexed message id for friend
"FROM history countHistory " // Import unfiltered table as countHistory
"JOIN peers chat ON chat_id = chat.id " // link chat_id to chat.id
"WHERE chat.public_key = '%1'" // filter this conversation
"AND countHistory.id <= history.id") // and filter that our unfiltered table history id only has elements up to history.id
.arg(friendPkString);
auto limitString = (maxNum) ? QString("LIMIT %1").arg(maxNum) : QString("");
auto queryString = QString("SELECT (%1), (timestamp / 1000 / 60 / 60 / 24) AS day "
"FROM history "
"JOIN peers chat ON chat_id = chat.id "
"WHERE chat.public_key = '%2' "
"AND timestamp >= %3 "
"GROUP by day "
"%4;")
.arg(countMessagesForFriend)
.arg(friendPkString)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
.arg(QDateTime(from.startOfDay()).toMSecsSinceEpoch())
#else
.arg(QDateTime(from).toMSecsSinceEpoch())
#endif
.arg(limitString);
QList<DateIdx> dateIdxs;
auto rowCallback = [&dateIdxs](const QVector<QVariant>& row) {
DateIdx dateIdx;
@ -809,7 +825,39 @@ QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(con
dateIdxs.append(dateIdx);
};
db->execNow({queryString, rowCallback});
// No guarantee that this is the most efficient way to do this...
// We want to count messages that happened for a friend before a
// certain date. We do this by re-joining our table a second time
// but this time with the only filter being that our id is less than
// the ID of the corresponding row in the table that is grouped by day
auto countMessagesForFriend =
QStringLiteral("SELECT COUNT(*) - 1 " // Count - 1 corresponds to 0 indexed message id for friend
"FROM history countHistory " // Import unfiltered table as countHistory
"JOIN chats ON chat_id = chats.id " // link chat_id to chat.id
"WHERE chats.uuid = ?" // filter this conversation
"AND countHistory.id <= history.id"); // and filter that our unfiltered table history id only has elements up to history.id
auto limitString = (maxNum) ? QString("LIMIT %1").arg(maxNum) : QString("");
auto query = RawDatabase::Query(QStringLiteral(
"SELECT (%1), (timestamp / 1000 / 60 / 60 / 24) AS day "
"FROM history "
"JOIN chats ON chat_id = chats.id "
"WHERE chats.uuid = ? "
"AND timestamp >= %2 "
"GROUP by day "
"%3;")
.arg(countMessagesForFriend)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
.arg(QDateTime(from.startOfDay()).toMSecsSinceEpoch())
#else
.arg(QDateTime(from).toMSecsSinceEpoch())
#endif
.arg(limitString),
{friendPk.getByteArray(), friendPk.getByteArray()},
rowCallback);
db->execNow(query);
return dateIdxs;
}

View File

@ -138,8 +138,8 @@ class History : public QObject, public std::enable_shared_from_this<History>
public:
struct HistMessage
{
HistMessage(RowId id_, MessageState state_, ExtensionSet extensionSet_, QDateTime timestamp_, QString chat_, QString dispName_,
QString sender_, QString message)
HistMessage(RowId id_, MessageState state_, ExtensionSet extensionSet_, QDateTime timestamp_, ToxPk chat_, QString dispName_,
ToxPk sender_, QString message)
: chat{chat_}
, sender{sender_}
, dispName{dispName_}
@ -150,8 +150,8 @@ public:
, content(std::move(message))
{}
HistMessage(RowId id_, MessageState state_, QDateTime timestamp_, QString chat_, QString dispName_,
QString sender_, ToxFile file)
HistMessage(RowId id_, MessageState state_, QDateTime timestamp_, ToxPk chat_, QString dispName_,
ToxPk sender_, ToxFile file)
: chat{chat_}
, sender{sender_}
, dispName{dispName_}
@ -161,7 +161,7 @@ public:
, content(std::move(file))
{}
HistMessage(RowId id_, QDateTime timestamp_, QString chat_, SystemMessage systemMessage)
HistMessage(RowId id_, QDateTime timestamp_, ToxPk chat_, SystemMessage systemMessage)
: chat{chat_}
, timestamp{timestamp_}
, id{id_}
@ -169,8 +169,8 @@ public:
, content(std::move(systemMessage))
{}
QString chat;
QString sender;
ToxPk chat;
ToxPk sender;
QString dispName;
QDateTime timestamp;
RowId id;

View File

@ -46,6 +46,7 @@ namespace DbUtility
extern const std::vector<SqliteMasterEntry> schema7;
extern const std::vector<SqliteMasterEntry> schema9;
extern const std::vector<SqliteMasterEntry> schema10;
extern const std::vector<SqliteMasterEntry> schema11;
void createSchemaAtVersion(std::shared_ptr<RawDatabase> db, const std::vector<DbUtility::SqliteMasterEntry>& schema);
void verifyDb(std::shared_ptr<RawDatabase> db, const std::vector<DbUtility::SqliteMasterEntry>& expectedSql);

View File

@ -95,7 +95,7 @@ const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema6 {
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
};
const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema7{
const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema7 {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB "
"NOT NULL, UNIQUE(owner, display_name), FOREIGN KEY (owner) REFERENCES peers(id))"},
{"faux_offline_pending",
@ -128,6 +128,37 @@ const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema7{
const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema9 = DbUtility::schema7;
const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema10 = DbUtility::schema9;
const std::vector<DbUtility::SqliteMasterEntry> DbUtility::schema11{
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB "
"NOT NULL, UNIQUE(owner, display_name), FOREIGN KEY (owner) REFERENCES authors(id))"},
{"faux_offline_pending",
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, required_extensions INTEGER NOT "
"NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"},
{"file_transfers",
"CREATE TABLE file_transfers (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL CHECK "
"(message_type = 'F'), sender_alias INTEGER NOT NULL, file_restart_id BLOB NOT NULL, "
"file_name BLOB NOT NULL, file_path BLOB NOT NULL, file_hash BLOB NOT NULL, file_size INTEGER "
"NOT NULL, direction INTEGER NOT NULL, file_state INTEGER NOT NULL, FOREIGN KEY (id, "
"message_type) REFERENCES history(id, message_type), FOREIGN KEY (sender_alias) REFERENCES "
"aliases(id))"},
{"history",
"CREATE TABLE history (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL DEFAULT 'T' "
"CHECK (message_type in ('T','F','S')), timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, "
"UNIQUE (id, message_type), FOREIGN KEY (chat_id) REFERENCES chats(id))"},
{"text_messages", "CREATE TABLE text_messages (id INTEGER PRIMARY KEY, message_type CHAR(1) "
"NOT NULL CHECK (message_type = 'T'), sender_alias INTEGER NOT NULL, message "
"BLOB NOT NULL, FOREIGN KEY (id, message_type) REFERENCES history(id, "
"message_type), FOREIGN KEY (sender_alias) REFERENCES aliases(id))"},
{"chats", "CREATE TABLE chats (id INTEGER PRIMARY KEY, uuid BLOB NOT NULL UNIQUE)"},
{"authors", "CREATE TABLE authors (id INTEGER PRIMARY KEY, public_key BLOB NOT NULL UNIQUE)"},
{"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, reason INTEGER NOT "
"NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"},
{"system_messages",
"CREATE TABLE system_messages (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL CHECK "
"(message_type = 'S'), system_message_type INTEGER NOT NULL, arg1 BLOB, arg2 BLOB, arg3 BLOB, arg4 BLOB, "
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type))"},
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}};
void DbUtility::createSchemaAtVersion(std::shared_ptr<RawDatabase> db, const std::vector<DbUtility::SqliteMasterEntry>& schema)
{
QVector<RawDatabase::Query> queries;

View File

@ -90,6 +90,7 @@ private slots:
// test7to8 omitted, version only upgrade, versions are not verified in this
// test8to9 omitted, data corruption correction upgrade with no schema change
void test9to10();
// test10to11 handled in dbTo11_test
// test suite
private:
@ -115,7 +116,7 @@ void TestDbSchema::testCreation()
QVector<RawDatabase::Query> queries;
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{testDatabaseFile->fileName(), {}, {}}};
QVERIFY(DbUpgrader::createCurrentSchema(*db));
DbUtility::verifyDb(db, DbUtility::schema7);
DbUtility::verifyDb(db, DbUtility::schema11);
}
void TestDbSchema::testIsNewDb()

View File

@ -0,0 +1,339 @@
/*
Copyright © 2022 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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.
qTox 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 qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "src/core/toxpk.h"
#include "src/persistence/db/rawdatabase.h"
#include "src/persistence/db/upgrades/dbto11.h"
#include "dbutility/dbutility.h"
#include <QByteArray>
#include <QTemporaryFile>
#include <QTest>
namespace
{
const auto selfPk = ToxPk{QByteArray(32, 0)};
const auto aPk = ToxPk{QByteArray(32, 1)};
const auto bPk = ToxPk{QByteArray(32, 2)};
const auto cPk = ToxPk{QByteArray(32, 3)};
const auto selfName = QStringLiteral("Self");
const auto aName = QStringLiteral("Alice");
const auto bName = QStringLiteral("Bob");
const auto selfAliasId = 1;
const auto aPeerId = 2;
const auto aChatId = 1;
const auto aAliasId = 2;
const auto bPeerId = 3;
const auto bChatId = 2;
const auto bAliasId = 3;
const auto cPeerId = 4;
const auto cChatId = 3;
void appendAddPeersQueries(QVector<RawDatabase::Query>& setupQueries)
{
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO peers (id, public_key) VALUES (1, ?)"),
{selfPk.toString().toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO peers (id, public_key) VALUES (2, ?)"),
{aPk.toString().toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO peers (id, public_key) VALUES (3, ?)"),
{bPk.toString().toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO peers (id, public_key) VALUES (4, ?)"),
{cPk.toString().toUtf8()}});
}
void appendVerifyChatsQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM chats"),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 3);
}});
struct Functor {
int index = 0;
const std::vector<QByteArray> chatIds{aPk.getByteArray(), bPk.getByteArray(), cPk.getByteArray()};
void operator()(const QVector<QVariant>& row) {
QVERIFY(row[0].toByteArray() == chatIds[index]);
++index;
}
};
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT uuid FROM chats"),
Functor()});
}
void appendVerifyAuthorsQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM authors"),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 3);
}});
struct Functor {
int index = 0;
const std::vector<QByteArray> chatIds{selfPk.getByteArray(), aPk.getByteArray(), bPk.getByteArray()};
void operator()(const QVector<QVariant>& row) {
QVERIFY(row[0].toByteArray() == chatIds[index]);
++index;
}
};
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT public_key FROM authors"),
Functor()});
}
void appendAddAliasesQueries(QVector<RawDatabase::Query>& setupQueries)
{
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO aliases (id, owner, display_name) VALUES (1, 1, ?)"),
{selfName.toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO aliases (id, owner, display_name) VALUES (2, 2, ?)"),
{aName.toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO aliases (id, owner, display_name) VALUES (3, 3, ?)"),
{bName.toUtf8()}});
}
void appendVerifyAliasesQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM aliases"),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 3);
}});
struct Functor {
int index = 0;
const std::vector<QByteArray> names{selfName.toUtf8(), aName.toUtf8(), bName.toUtf8()};
void operator()(const QVector<QVariant>& row) {
QVERIFY(row[0].toByteArray() == names[index]);
++index;
}
};
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT display_name FROM aliases"),
Functor()});
}
void appendAddAChatMessagesQueries(QVector<RawDatabase::Query>& setupQueries)
{
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (1, 'T', 0, '%1')").arg(aPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (1, 'T', '%1', ?)").arg(aAliasId),
{QStringLiteral("Message 1 from A to Self").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (2, 'T', 0, '%1')").arg(aPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (2, 'T', '%1', ?)").arg(aAliasId),
{QStringLiteral("Message 2 from A to Self").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (10, 'F', 0, '%1')").arg(aPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO file_transfers (id, message_type, sender_alias, file_restart_id, "
"file_name, file_path, file_hash, file_size, direction, file_state) "
"VALUES(10, 'F', '%1', ?, 'dummy name', 'dummy/path', ?, 1024, 1, 5)").arg(aAliasId),
{QByteArray(32, 1), QByteArray(32, 2)}});
}
void appendVerifyAChatMessagesQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history WHERE chat_id = '%1'").arg(aChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 3);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM text_messages WHERE sender_alias = '%1'").arg(aAliasId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 2);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM file_transfers WHERE sender_alias = '%1'").arg(aAliasId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 1);
}});
}
void appendAddBChatMessagesQueries(QVector<RawDatabase::Query>& setupQueries)
{
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (3, 'T', 0, '%1')").arg(bPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (3, 'T', '%1', ?)").arg(bAliasId),
{QStringLiteral("Message 1 from B to Self").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (4, 'T', 0, '%1')").arg(bPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (4, 'T', '%1', ?)").arg(selfAliasId),
{QStringLiteral("Message 1 from Self to B").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (5, 'T', 0, '%1')").arg(bPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (5, 'T', '%1', ?)").arg(selfAliasId),
{QStringLiteral("Pending message 1 from Self to B").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO faux_offline_pending (id) VALUES (5)")});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (8, 'S', 0, '%1')").arg(bPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO system_messages (id, message_type, system_message_type) VALUES (8, 'S', 1)")});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (9, 'F', 0, '%1')").arg(bPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO file_transfers (id, message_type, sender_alias, file_restart_id, "
"file_name, file_path, file_hash, file_size, direction, file_state) "
"VALUES(9, 'F', '%1', ?, 'dummy name', 'dummy/path', ?, 1024, 0, 5)").arg(selfAliasId),
{QByteArray(32, 1), QByteArray(32, 2)}});
}
void appendVerifyBChatMessagesQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history WHERE chat_id = '%1'").arg(bChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 5);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history JOIN text_messages ON "
"history.id = text_messages.id WHERE chat_id = '%1'").arg(bChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 3);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history JOIN faux_offline_pending ON "
"history.id = faux_offline_pending.id WHERE chat_id = '%1'").arg(bChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 1);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM file_transfers WHERE sender_alias = '%1'").arg(selfAliasId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 1);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history JOIN system_messages ON "
"history.id = system_messages.id WHERE chat_id = '%1'").arg(bChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 1);
}});
}
void appendAddCChatMessagesQueries(QVector<RawDatabase::Query>& setupQueries)
{
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (6, 'T', 0, '%1')").arg(cPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (6, 'T', '%1', ?)").arg(selfAliasId),
{QStringLiteral("Message 1 from Self to B").toUtf8()}});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO broken_messages (id) VALUES (6)")});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO history (id, message_type, timestamp, chat_id) VALUES (7, 'T', 0, '%1')").arg(cPeerId)});
setupQueries.append(RawDatabase::Query{QStringLiteral(
"INSERT INTO text_messages (id, message_type, sender_alias, message) VALUES (7, 'T', '%1', ?)").arg(selfAliasId),
{QStringLiteral("Message 1 from Self to B").toUtf8()}});
}
void appendVerifyCChatMessagesQueries(QVector<RawDatabase::Query>& verifyQueries)
{
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history WHERE chat_id = '%1'").arg(cChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 2);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history JOIN broken_messages ON "
"history.id = broken_messages.id WHERE chat_id = '%1'").arg(cChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 1);
}});
verifyQueries.append(RawDatabase::Query{QStringLiteral(
"SELECT COUNT(*) FROM history JOIN text_messages ON "
"history.id = text_messages.id WHERE chat_id = '%1'").arg(cChatId),
[&](const QVector<QVariant>& row) {
QVERIFY(row[0].toLongLong() == 2);
}});
}
void appendAddHistoryQueries(QVector<RawDatabase::Query>& setupQueries)
{
appendAddAChatMessagesQueries(setupQueries);
appendAddBChatMessagesQueries(setupQueries);
appendAddCChatMessagesQueries(setupQueries);
}
void appendVerifyHistoryQueries(QVector<RawDatabase::Query>& verifyQueries)
{
appendVerifyAChatMessagesQueries(verifyQueries);
appendVerifyBChatMessagesQueries(verifyQueries);
appendVerifyCChatMessagesQueries(verifyQueries);
}
} // namespace
class Test10to11 : public QObject
{
Q_OBJECT
private slots:
void test10to11();
private:
QTemporaryFile testDatabaseFile;
};
void Test10to11::test10to11()
{
QVERIFY(testDatabaseFile.open());
testDatabaseFile.close();
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{testDatabaseFile.fileName(), {}, {}}};
QVERIFY(db->execNow(RawDatabase::Query{QStringLiteral("PRAGMA foreign_keys = ON;")}));
createSchemaAtVersion(db, DbUtility::schema10);
QVector<RawDatabase::Query> setupQueries;
appendAddPeersQueries(setupQueries);
appendAddAliasesQueries(setupQueries);
appendAddHistoryQueries(setupQueries);
QVERIFY(db->execNow(setupQueries));
QVERIFY(DbTo11::dbSchema10to11(*db));
verifyDb(db, DbUtility::schema11);
QVector<RawDatabase::Query> verifyQueries;
appendVerifyChatsQueries(verifyQueries);
appendVerifyAuthorsQueries(verifyQueries);
appendVerifyAliasesQueries(verifyQueries);
appendVerifyHistoryQueries(verifyQueries);
QVERIFY(db->execNow(verifyQueries));
}
QTEST_GUILESS_MAIN(Test10to11)
#include "dbTo11_test.moc"