mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
refactor(history): Separate db upgrade logic from History
Allows for cleaner testability of upgrade logic and reduces the overall size and clutter of History.
This commit is contained in:
parent
2a2b079992
commit
d7b67081e5
|
@ -317,6 +317,8 @@ set(${PROJECT_NAME}_SOURCES
|
|||
src/persistence/db/rawdatabase.h
|
||||
src/persistence/history.cpp
|
||||
src/persistence/history.h
|
||||
src/persistence/dbupgrader.cpp
|
||||
src/persistence/dbupgrader.h
|
||||
src/persistence/ifriendsettings.cpp
|
||||
src/persistence/ifriendsettings.h
|
||||
src/persistence/igroupsettings.cpp
|
||||
|
|
612
src/persistence/dbupgrader.cpp
Normal file
612
src/persistence/dbupgrader.cpp
Normal file
|
@ -0,0 +1,612 @@
|
|||
/*
|
||||
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 "dbupgrader.h"
|
||||
#include "src/core/chatid.h"
|
||||
#include "src/core/toxpk.h"
|
||||
#include "src/persistence/db/rawdatabase.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
namespace {
|
||||
constexpr int SCHEMA_VERSION = 9;
|
||||
|
||||
struct BadEntry
|
||||
{
|
||||
BadEntry(int64_t row_, QString toxId_)
|
||||
: row{row_}
|
||||
, toxId{toxId_}
|
||||
{}
|
||||
RowId row;
|
||||
QString toxId;
|
||||
};
|
||||
|
||||
std::vector<BadEntry> getInvalidPeers(RawDatabase& db)
|
||||
{
|
||||
std::vector<BadEntry> badPeerIds;
|
||||
db.execNow(
|
||||
RawDatabase::Query("SELECT id, public_key FROM peers WHERE LENGTH(public_key) != 64",
|
||||
[&](const QVector<QVariant>& row) {
|
||||
badPeerIds.emplace_back(BadEntry{row[0].toInt(), row[1].toString()});
|
||||
}));
|
||||
return badPeerIds;
|
||||
}
|
||||
|
||||
RowId getValidPeerRow(RawDatabase& db, const ChatId& chatId)
|
||||
{
|
||||
bool validPeerExists{false};
|
||||
RowId validPeerRow;
|
||||
db.execNow(RawDatabase::Query(QStringLiteral("SELECT id FROM peers WHERE public_key='%1';")
|
||||
.arg(chatId.toString()),
|
||||
[&](const QVector<QVariant>& row) {
|
||||
validPeerRow = RowId{row[0].toLongLong()};
|
||||
validPeerExists = true;
|
||||
}));
|
||||
if (validPeerExists) {
|
||||
return validPeerRow;
|
||||
}
|
||||
|
||||
db.execNow(RawDatabase::Query(("SELECT id FROM peers ORDER BY id DESC LIMIT 1;"),
|
||||
[&](const QVector<QVariant>& row) {
|
||||
int64_t maxPeerId = row[0].toInt();
|
||||
validPeerRow = RowId{maxPeerId + 1};
|
||||
}));
|
||||
db.execNow(
|
||||
RawDatabase::Query(QStringLiteral("INSERT INTO peers (id, public_key) VALUES (%1, '%2');")
|
||||
.arg(validPeerRow.get())
|
||||
.arg(chatId.toString())));
|
||||
return validPeerRow;
|
||||
}
|
||||
|
||||
struct DuplicateAlias
|
||||
{
|
||||
DuplicateAlias(RowId goodAliasRow_, std::vector<RowId> badAliasRows_)
|
||||
: goodAliasRow{goodAliasRow_}
|
||||
, badAliasRows{badAliasRows_}
|
||||
{}
|
||||
DuplicateAlias(){};
|
||||
RowId goodAliasRow{-1};
|
||||
std::vector<RowId> badAliasRows;
|
||||
};
|
||||
|
||||
DuplicateAlias getDuplicateAliasRows(RawDatabase& db, RowId goodPeerRow, RowId badPeerRow)
|
||||
{
|
||||
std::vector<RowId> badAliasRows;
|
||||
RowId goodAliasRow;
|
||||
bool hasGoodEntry{false};
|
||||
db.execNow(RawDatabase::Query(
|
||||
QStringLiteral("SELECT good.id, bad.id FROM aliases good INNER JOIN aliases bad ON "
|
||||
"good.display_name=bad.display_name WHERE good.owner=%1 AND bad.owner=%2;")
|
||||
.arg(goodPeerRow.get())
|
||||
.arg(badPeerRow.get()),
|
||||
[&](const QVector<QVariant>& row) {
|
||||
hasGoodEntry = true;
|
||||
goodAliasRow = RowId{row[0].toInt()};
|
||||
badAliasRows.emplace_back(RowId{row[1].toLongLong()});
|
||||
}));
|
||||
|
||||
if (hasGoodEntry) {
|
||||
return {goodAliasRow, badAliasRows};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void mergeAndDeleteAlias(QVector<RawDatabase::Query>& upgradeQueries, RowId goodAlias,
|
||||
std::vector<RowId> badAliases)
|
||||
{
|
||||
for (const auto badAliasId : badAliases) {
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
QStringLiteral("UPDATE text_messages SET sender_alias = %1 WHERE sender_alias = %2;")
|
||||
.arg(goodAlias.get())
|
||||
.arg(badAliasId.get()));
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
QStringLiteral("UPDATE file_transfers SET sender_alias = %1 WHERE sender_alias = %2;")
|
||||
.arg(goodAlias.get())
|
||||
.arg(badAliasId.get()));
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
QStringLiteral("DELETE FROM aliases WHERE id = %1;").arg(badAliasId.get()));
|
||||
}
|
||||
}
|
||||
|
||||
void mergeAndDeletePeer(QVector<RawDatabase::Query>& upgradeQueries, RowId goodPeerId, RowId badPeerId)
|
||||
{
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query(QStringLiteral("UPDATE aliases SET owner = %1 WHERE owner = %2")
|
||||
.arg(goodPeerId.get())
|
||||
.arg(badPeerId.get()));
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query(QStringLiteral("UPDATE history SET chat_id = %1 WHERE chat_id = %2;")
|
||||
.arg(goodPeerId.get())
|
||||
.arg(badPeerId.get()));
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query(QStringLiteral("DELETE FROM peers WHERE id = %1").arg(badPeerId.get()));
|
||||
}
|
||||
|
||||
void mergeDuplicatePeers(QVector<RawDatabase::Query>& upgradeQueries, RawDatabase& db,
|
||||
std::vector<BadEntry> badPeers)
|
||||
{
|
||||
for (const auto& badPeer : badPeers) {
|
||||
const RowId goodPeerId = getValidPeerRow(db, ToxPk{badPeer.toxId.left(64)});
|
||||
const auto aliasDuplicates = getDuplicateAliasRows(db, goodPeerId, badPeer.row);
|
||||
mergeAndDeleteAlias(upgradeQueries, aliasDuplicates.goodAliasRow, aliasDuplicates.badAliasRows);
|
||||
mergeAndDeletePeer(upgradeQueries, goodPeerId, badPeer.row);
|
||||
}
|
||||
}
|
||||
|
||||
void addForeignKeyToAlias(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += 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 peers(id));"));
|
||||
queries +=
|
||||
RawDatabase::Query(QStringLiteral("INSERT INTO aliases_new (id, owner, display_name) "
|
||||
"SELECT id, owner, display_name "
|
||||
"FROM aliases;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE aliases;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE aliases_new RENAME TO aliases;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToHistory(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries +=
|
||||
RawDatabase::Query(QStringLiteral("CREATE TABLE history_new "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"timestamp INTEGER NOT NULL, "
|
||||
"chat_id INTEGER NOT NULL, "
|
||||
"sender_alias INTEGER NOT NULL, "
|
||||
"message BLOB NOT NULL, "
|
||||
"file_id INTEGER, "
|
||||
"FOREIGN KEY (file_id) REFERENCES file_transfers(id), "
|
||||
"FOREIGN KEY (chat_id) REFERENCES peers(id), "
|
||||
"FOREIGN KEY (sender_alias) REFERENCES aliases(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO history_new (id, timestamp, chat_id, sender_alias, message, file_id) "
|
||||
"SELECT id, timestamp, chat_id, sender_alias, message, file_id "
|
||||
"FROM history;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE history;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history_new RENAME TO history;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToFauxOfflinePending(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += RawDatabase::Query(
|
||||
QStringLiteral("CREATE TABLE new_faux_offline_pending (id INTEGER PRIMARY KEY, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral("INSERT INTO new_faux_offline_pending (id) "
|
||||
"SELECT id "
|
||||
"FROM faux_offline_pending;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE faux_offline_pending;"));
|
||||
queries += RawDatabase::Query(
|
||||
QStringLiteral("ALTER TABLE new_faux_offline_pending RENAME TO faux_offline_pending;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToBrokenMessages(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += RawDatabase::Query(
|
||||
QStringLiteral("CREATE TABLE new_broken_messages (id INTEGER PRIMARY KEY, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral("INSERT INTO new_broken_messages (id) "
|
||||
"SELECT id "
|
||||
"FROM broken_messages;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE broken_messages;"));
|
||||
queries += RawDatabase::Query(
|
||||
QStringLiteral("ALTER TABLE new_broken_messages RENAME TO broken_messages;"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Upgrade the db schema
|
||||
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
|
||||
* variable and add another case to the switch statement below. Make sure to fall through on each case.
|
||||
*/
|
||||
bool DbUpgrader::dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
|
||||
{
|
||||
// If we're a new dB we can just make a new one and call it a day
|
||||
bool success = false;
|
||||
const bool newDb = isNewDb(db, success);
|
||||
if (!success) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newDb) {
|
||||
if (!createCurrentSchema(*db)) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we have to do upgrades from our current version to the latest version
|
||||
|
||||
int64_t databaseSchemaVersion;
|
||||
|
||||
if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
|
||||
databaseSchemaVersion = row[0].toLongLong();
|
||||
}))) {
|
||||
qCritical() << "History failed to read user_version";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion > SCHEMA_VERSION) {
|
||||
qWarning().nospace() << "Database version (" << databaseSchemaVersion
|
||||
<< ") is newer than we currently support (" << SCHEMA_VERSION
|
||||
<< "). Please upgrade qTox";
|
||||
// We don't know what future versions have done, we have to disable db access until we re-upgrade
|
||||
return false;
|
||||
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
|
||||
// No work to do
|
||||
return true;
|
||||
}
|
||||
|
||||
using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
|
||||
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
|
||||
dbSchema3to4, dbSchema4to5, dbSchema5to6,
|
||||
dbSchema6to7, dbSchema7to8, dbSchema8to9};
|
||||
|
||||
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
|
||||
assert(upgradeFns.size() == SCHEMA_VERSION);
|
||||
|
||||
for (int64_t i = databaseSchemaVersion; i < static_cast<int>(upgradeFns.size()); ++i) {
|
||||
auto const newDbVersion = i + 1;
|
||||
if (!upgradeFns[i](*db)) {
|
||||
qCritical() << "Failed to upgrade db to schema version " << newDbVersion << " aborting";
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Database upgraded incrementally to schema version " << newDbVersion;
|
||||
}
|
||||
|
||||
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion << "->"
|
||||
<< SCHEMA_VERSION << ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
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 aliases (id INTEGER PRIMARY KEY, "
|
||||
"owner INTEGER, "
|
||||
"display_name BLOB NOT NULL, "
|
||||
"UNIQUE(owner, display_name), "
|
||||
"FOREIGN KEY (owner) REFERENCES peers(id));"
|
||||
"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, "
|
||||
// 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)); "
|
||||
"CREATE TABLE text_messages "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "
|
||||
"sender_alias INTEGER NOT NULL, "
|
||||
// even though technically a message can be null for file transfer, we've opted
|
||||
// to just insert an empty string when there's no content, this moderately simplifies
|
||||
// implementation as currently our database doesn't have support for optional fields.
|
||||
// We would either have to insert "?" or "null" based on if message exists and then
|
||||
// ensure that our blob vector always has the right number of fields. Better to just
|
||||
// leave this as NOT NULL for now.
|
||||
"message BLOB NOT NULL, "
|
||||
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
|
||||
"FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
|
||||
"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)); "
|
||||
"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)); "
|
||||
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, "
|
||||
"required_extensions INTEGER NOT NULL DEFAULT 0, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"
|
||||
"CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, "
|
||||
"reason INTEGER NOT NULL DEFAULT 0, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
// sqlite doesn't support including the index as part of the CREATE TABLE statement, so add a second query
|
||||
queries += RawDatabase::Query(
|
||||
"CREATE INDEX chat_id_idx on history (chat_id);");
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
|
||||
{
|
||||
bool newDb;
|
||||
if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
|
||||
[&](const QVector<QVariant>& row) {
|
||||
newDb = row[0].toLongLong() == 0;
|
||||
}))) {
|
||||
db.reset();
|
||||
success = false;
|
||||
return false;
|
||||
}
|
||||
success = true;
|
||||
return newDb;
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema0to1(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
queries += RawDatabase::Query(QStringLiteral("CREATE TABLE file_transfers "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"chat_id 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);"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema1to2(RawDatabase& db)
|
||||
{
|
||||
// Any faux_offline_pending message, in a chat that has newer delivered
|
||||
// message is decided to be broken. It must be moved from
|
||||
// faux_offline_pending to broken_messages
|
||||
|
||||
// the last non-pending message in each chat
|
||||
QString lastDeliveredQuery =
|
||||
QString("SELECT chat_id, MAX(history.id) FROM "
|
||||
"history JOIN peers chat ON chat_id = chat.id "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"WHERE faux_offline_pending.id IS NULL "
|
||||
"GROUP BY chat_id;");
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("CREATE TABLE broken_messages "
|
||||
"(id INTEGER PRIMARY KEY);"));
|
||||
|
||||
auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
|
||||
auto chatId = row[0].toLongLong();
|
||||
auto lastDeliveredHistoryId = row[1].toLongLong();
|
||||
|
||||
upgradeQueries += QString("INSERT INTO broken_messages "
|
||||
"SELECT faux_offline_pending.id FROM "
|
||||
"history JOIN faux_offline_pending "
|
||||
"ON faux_offline_pending.id = history.id "
|
||||
"WHERE history.chat_id=%1 "
|
||||
"AND history.id < %2;")
|
||||
.arg(chatId)
|
||||
.arg(lastDeliveredHistoryId);
|
||||
};
|
||||
// note this doesn't modify the db, just generate new queries, so is safe
|
||||
// to run outside of our upgrade transaction
|
||||
if (!db.execNow({lastDeliveredQuery, rowCallback})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
upgradeQueries += QString("DELETE FROM faux_offline_pending "
|
||||
"WHERE id in ("
|
||||
"SELECT id FROM broken_messages);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema2to3(RawDatabase& db)
|
||||
{
|
||||
// Any faux_offline_pending message with the content "/me " are action
|
||||
// messages that qTox previously let a user enter, but that will cause an
|
||||
// action type message to be sent to toxcore, with 0 length, which will
|
||||
// always fail. They must be be moved from faux_offline_pending to broken_messages
|
||||
// to avoid qTox from erroring trying to send them on every connect
|
||||
|
||||
const QString emptyActionMessageString = "/me ";
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query{QString("INSERT INTO broken_messages "
|
||||
"SELECT faux_offline_pending.id FROM "
|
||||
"history JOIN faux_offline_pending "
|
||||
"ON faux_offline_pending.id = history.id "
|
||||
"WHERE history.message = ?;"),
|
||||
{emptyActionMessageString.toUtf8()}};
|
||||
|
||||
upgradeQueries += QString("DELETE FROM faux_offline_pending "
|
||||
"WHERE id in ("
|
||||
"SELECT id FROM broken_messages);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 3;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema3to4(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query{QString("CREATE INDEX chat_id_idx on history (chat_id);")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 4;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema4to5(RawDatabase& db)
|
||||
{
|
||||
// add foreign key contrains to database tables. sqlite doesn't support advanced alter table
|
||||
// commands, so instead we need to copy data to new tables with the foreign key contraints:
|
||||
// http://www.sqlitetutorial.net/sqlite-alter-table/
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
addForeignKeyToAlias(upgradeQueries);
|
||||
addForeignKeyToHistory(upgradeQueries);
|
||||
addForeignKeyToFauxOfflinePending(upgradeQueries);
|
||||
addForeignKeyToBrokenMessages(upgradeQueries);
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 5;"));
|
||||
auto transactionPass = db.execNow(upgradeQueries);
|
||||
if (transactionPass) {
|
||||
db.execNow("VACUUM;"); // after copying all the tables and deleting the old ones, our db file is half empty.
|
||||
}
|
||||
return transactionPass;
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema5to6(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
|
||||
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE faux_offline_pending "
|
||||
"ADD COLUMN required_extensions INTEGER NOT NULL "
|
||||
"DEFAULT 0;")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE broken_messages "
|
||||
"ADD COLUMN reason INTEGER NOT NULL "
|
||||
"DEFAULT 0;")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 6;"));
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema6to7(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
|
||||
// Cannot add UNIQUE(id, message_type) to history table without creating a new one. Create a new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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 peers(id))");
|
||||
|
||||
// Create new text_messages table. We will split messages out of history and insert them into this new table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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_new(id, message_type), FOREIGN KEY (sender_alias) "
|
||||
"REFERENCES aliases(id))");
|
||||
|
||||
// Cannot add a FOREIGN KEY to the file_transfers table without creating a new one. Create a new file_transfers table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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("INSERT INTO history_new SELECT id, 'T' AS message_type, timestamp, "
|
||||
"chat_id FROM history WHERE history.file_id IS NULL");
|
||||
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("INSERT INTO text_messages SELECT id, 'T' AS message_type, "
|
||||
"sender_alias, message FROM history WHERE history.file_id IS NULL");
|
||||
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("INSERT INTO history_new SELECT id, 'F' AS message_type, timestamp, "
|
||||
"chat_id FROM history WHERE history.file_id IS NOT NULL");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"INSERT INTO file_transfers_new (id, message_type, sender_alias, file_restart_id, "
|
||||
"file_name, file_path, file_hash, file_size, direction, file_state) SELECT history.id, 'F' "
|
||||
"as message_type, history.sender_alias, file_transfers.file_restart_id, "
|
||||
"file_transfers.file_name, file_transfers.file_path, file_transfers.file_hash, "
|
||||
"file_transfers.file_size, file_transfers.direction, file_transfers.file_state FROM "
|
||||
"history INNER JOIN file_transfers on history.file_id = file_transfers.id WHERE "
|
||||
"history.file_id IS NOT NULL");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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_new(id, message_type))");
|
||||
|
||||
// faux_offline_pending needs to be re-created to reference the new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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("INSERT INTO faux_offline_pending_new SELECT id, "
|
||||
"required_extensions FROM faux_offline_pending");
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE faux_offline_pending");
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("ALTER TABLE faux_offline_pending_new RENAME TO faux_offline_pending");
|
||||
|
||||
// broken_messages needs to be re-created to reference the new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"CREATE TABLE broken_messages_new (id INTEGER PRIMARY KEY, reason INTEGER NOT NULL DEFAULT "
|
||||
"0, FOREIGN KEY (id) REFERENCES history_new(id))");
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"INSERT INTO broken_messages_new SELECT id, reason FROM broken_messages");
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE broken_messages");
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("ALTER TABLE broken_messages_new RENAME TO broken_messages");
|
||||
|
||||
// Everything referencing old history should now be gone
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE history");
|
||||
upgradeQueries += RawDatabase::Query("ALTER TABLE history_new RENAME TO history");
|
||||
|
||||
// Drop file transfers late since history depends on it
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE file_transfers");
|
||||
upgradeQueries += RawDatabase::Query("ALTER TABLE file_transfers_new RENAME TO file_transfers");
|
||||
|
||||
upgradeQueries += RawDatabase::Query("CREATE INDEX chat_id_idx on history (chat_id);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 7;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema7to8(RawDatabase& db)
|
||||
{
|
||||
// Dummy upgrade. This upgrade does not change the schema, however on
|
||||
// version 7 if qtox saw a system message it would assert and crash. This
|
||||
// upgrade ensures that old versions of qtox do not try to load the new
|
||||
// database
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 8;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool DbUpgrader::dbSchema8to9(RawDatabase& db)
|
||||
{
|
||||
// not technically a schema update, but still a database version update based on healing invalid user data
|
||||
// we added ourself in the peers table by ToxId isntead of ToxPk. Heal this over-length entry.
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
const auto badPeers = getInvalidPeers(db);
|
||||
mergeDuplicatePeers(upgradeQueries, db, badPeers);
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 9;"));
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
40
src/persistence/dbupgrader.h
Normal file
40
src/persistence/dbupgrader.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
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 <memory>
|
||||
|
||||
class RawDatabase;
|
||||
namespace DbUpgrader
|
||||
{
|
||||
bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db);
|
||||
|
||||
bool createCurrentSchema(RawDatabase& db);
|
||||
bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success);
|
||||
bool dbSchema0to1(RawDatabase& db);
|
||||
bool dbSchema1to2(RawDatabase& db);
|
||||
bool dbSchema2to3(RawDatabase& db);
|
||||
bool dbSchema3to4(RawDatabase& db);
|
||||
bool dbSchema4to5(RawDatabase& db);
|
||||
bool dbSchema5to6(RawDatabase& db);
|
||||
bool dbSchema6to7(RawDatabase& db);
|
||||
bool dbSchema7to8(RawDatabase& db);
|
||||
bool dbSchema8to9(RawDatabase& db);
|
||||
}
|
|
@ -23,562 +23,11 @@
|
|||
#include "history.h"
|
||||
#include "profile.h"
|
||||
#include "settings.h"
|
||||
#include "dbupgrader.h"
|
||||
#include "db/rawdatabase.h"
|
||||
#include "src/core/toxpk.h"
|
||||
|
||||
namespace {
|
||||
constexpr int SCHEMA_VERSION = 9;
|
||||
|
||||
bool 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 aliases (id INTEGER PRIMARY KEY, "
|
||||
"owner INTEGER, "
|
||||
"display_name BLOB NOT NULL, "
|
||||
"UNIQUE(owner, display_name), "
|
||||
"FOREIGN KEY (owner) REFERENCES peers(id));"
|
||||
"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, "
|
||||
// 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)); "
|
||||
"CREATE TABLE text_messages "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "
|
||||
"sender_alias INTEGER NOT NULL, "
|
||||
// even though technically a message can be null for file transfer, we've opted
|
||||
// to just insert an empty string when there's no content, this moderately simplifies
|
||||
// implementation as currently our database doesn't have support for optional fields.
|
||||
// We would either have to insert "?" or "null" based on if message exists and then
|
||||
// ensure that our blob vector always has the right number of fields. Better to just
|
||||
// leave this as NOT NULL for now.
|
||||
"message BLOB NOT NULL, "
|
||||
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
|
||||
"FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
|
||||
"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)); "
|
||||
"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)); "
|
||||
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, "
|
||||
"required_extensions INTEGER NOT NULL DEFAULT 0, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"
|
||||
"CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, "
|
||||
"reason INTEGER NOT NULL DEFAULT 0, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
// sqlite doesn't support including the index as part of the CREATE TABLE statement, so add a second query
|
||||
queries += RawDatabase::Query(
|
||||
"CREATE INDEX chat_id_idx on history (chat_id);");
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
|
||||
{
|
||||
bool newDb;
|
||||
if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
|
||||
[&](const QVector<QVariant>& row) {
|
||||
newDb = row[0].toLongLong() == 0;
|
||||
}))) {
|
||||
db.reset();
|
||||
success = false;
|
||||
return false;
|
||||
}
|
||||
success = true;
|
||||
return newDb;
|
||||
}
|
||||
|
||||
bool dbSchema0to1(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
queries += RawDatabase::Query(QStringLiteral("CREATE TABLE file_transfers "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"chat_id 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);"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool dbSchema1to2(RawDatabase& db)
|
||||
{
|
||||
// Any faux_offline_pending message, in a chat that has newer delivered
|
||||
// message is decided to be broken. It must be moved from
|
||||
// faux_offline_pending to broken_messages
|
||||
|
||||
// the last non-pending message in each chat
|
||||
QString lastDeliveredQuery =
|
||||
QString("SELECT chat_id, MAX(history.id) FROM "
|
||||
"history JOIN peers chat ON chat_id = chat.id "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"WHERE faux_offline_pending.id IS NULL "
|
||||
"GROUP BY chat_id;");
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("CREATE TABLE broken_messages "
|
||||
"(id INTEGER PRIMARY KEY);"));
|
||||
|
||||
auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
|
||||
auto chatId = row[0].toLongLong();
|
||||
auto lastDeliveredHistoryId = row[1].toLongLong();
|
||||
|
||||
upgradeQueries += QString("INSERT INTO broken_messages "
|
||||
"SELECT faux_offline_pending.id FROM "
|
||||
"history JOIN faux_offline_pending "
|
||||
"ON faux_offline_pending.id = history.id "
|
||||
"WHERE history.chat_id=%1 "
|
||||
"AND history.id < %2;")
|
||||
.arg(chatId)
|
||||
.arg(lastDeliveredHistoryId);
|
||||
};
|
||||
// note this doesn't modify the db, just generate new queries, so is safe
|
||||
// to run outside of our upgrade transaction
|
||||
if (!db.execNow({lastDeliveredQuery, rowCallback})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
upgradeQueries += QString("DELETE FROM faux_offline_pending "
|
||||
"WHERE id in ("
|
||||
"SELECT id FROM broken_messages);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool dbSchema2to3(RawDatabase& db)
|
||||
{
|
||||
// Any faux_offline_pending message with the content "/me " are action
|
||||
// messages that qTox previously let a user enter, but that will cause an
|
||||
// action type message to be sent to toxcore, with 0 length, which will
|
||||
// always fail. They must be be moved from faux_offline_pending to broken_messages
|
||||
// to avoid qTox from erroring trying to send them on every connect
|
||||
|
||||
const QString emptyActionMessageString = "/me ";
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query{QString("INSERT INTO broken_messages "
|
||||
"SELECT faux_offline_pending.id FROM "
|
||||
"history JOIN faux_offline_pending "
|
||||
"ON faux_offline_pending.id = history.id "
|
||||
"WHERE history.message = ?;"),
|
||||
{emptyActionMessageString.toUtf8()}};
|
||||
|
||||
upgradeQueries += QString("DELETE FROM faux_offline_pending "
|
||||
"WHERE id in ("
|
||||
"SELECT id FROM broken_messages);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 3;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool dbSchema3to4(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query{QString(
|
||||
"CREATE INDEX chat_id_idx on history (chat_id);")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 4;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
void addForeignKeyToAlias(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += 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 peers(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO aliases_new (id, owner, display_name) "
|
||||
"SELECT id, owner, display_name "
|
||||
"FROM aliases;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE aliases;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE aliases_new RENAME TO aliases;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToHistory(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE history_new "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"timestamp INTEGER NOT NULL, "
|
||||
"chat_id INTEGER NOT NULL, "
|
||||
"sender_alias INTEGER NOT NULL, "
|
||||
"message BLOB NOT NULL, "
|
||||
"file_id INTEGER, "
|
||||
"FOREIGN KEY (file_id) REFERENCES file_transfers(id), "
|
||||
"FOREIGN KEY (chat_id) REFERENCES peers(id), "
|
||||
"FOREIGN KEY (sender_alias) REFERENCES aliases(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO history_new (id, timestamp, chat_id, sender_alias, message, file_id) "
|
||||
"SELECT id, timestamp, chat_id, sender_alias, message, file_id "
|
||||
"FROM history;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE history;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history_new RENAME TO history;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToFauxOfflinePending(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE new_faux_offline_pending (id INTEGER PRIMARY KEY, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO new_faux_offline_pending (id) "
|
||||
"SELECT id "
|
||||
"FROM faux_offline_pending;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE faux_offline_pending;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE new_faux_offline_pending RENAME TO faux_offline_pending;"));
|
||||
}
|
||||
|
||||
void addForeignKeyToBrokenMessages(QVector<RawDatabase::Query>& queries)
|
||||
{
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE new_broken_messages (id INTEGER PRIMARY KEY, "
|
||||
"FOREIGN KEY (id) REFERENCES history(id));"));
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO new_broken_messages (id) "
|
||||
"SELECT id "
|
||||
"FROM broken_messages;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("DROP TABLE broken_messages;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE new_broken_messages RENAME TO broken_messages;"));
|
||||
}
|
||||
|
||||
bool dbSchema4to5(RawDatabase& db)
|
||||
{
|
||||
// add foreign key contrains to database tables. sqlite doesn't support advanced alter table commands, so instead we
|
||||
// need to copy data to new tables with the foreign key contraints: http://www.sqlitetutorial.net/sqlite-alter-table/
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
addForeignKeyToAlias(upgradeQueries);
|
||||
addForeignKeyToHistory(upgradeQueries);
|
||||
addForeignKeyToFauxOfflinePending(upgradeQueries);
|
||||
addForeignKeyToBrokenMessages(upgradeQueries);
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 5;"));
|
||||
auto transactionPass = db.execNow(upgradeQueries);
|
||||
if (transactionPass) {
|
||||
db.execNow("VACUUM;"); // after copying all the tables and deleting the old ones, our db file is half empty.
|
||||
}
|
||||
return transactionPass;
|
||||
}
|
||||
|
||||
bool dbSchema5to6(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
|
||||
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE faux_offline_pending "
|
||||
"ADD COLUMN required_extensions INTEGER NOT NULL "
|
||||
"DEFAULT 0;")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE broken_messages "
|
||||
"ADD COLUMN reason INTEGER NOT NULL "
|
||||
"DEFAULT 0;")};
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 6;"));
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool dbSchema6to7(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
|
||||
// Cannot add UNIQUE(id, message_type) to history table without creating a new one. Create a new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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 peers(id))");
|
||||
|
||||
// Create new text_messages table. We will split messages out of history and insert them into this new table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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_new(id, message_type), FOREIGN KEY (sender_alias) "
|
||||
"REFERENCES aliases(id))");
|
||||
|
||||
// Cannot add a FOREIGN KEY to the file_transfers table without creating a new one. Create a new file_transfers table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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("INSERT INTO history_new SELECT id, 'T' AS message_type, timestamp, "
|
||||
"chat_id FROM history WHERE history.file_id IS NULL");
|
||||
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("INSERT INTO text_messages SELECT id, 'T' AS message_type, "
|
||||
"sender_alias, message FROM history WHERE history.file_id IS NULL");
|
||||
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("INSERT INTO history_new SELECT id, 'F' AS message_type, timestamp, "
|
||||
"chat_id FROM history WHERE history.file_id IS NOT NULL");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"INSERT INTO file_transfers_new (id, message_type, sender_alias, file_restart_id, "
|
||||
"file_name, file_path, file_hash, file_size, direction, file_state) SELECT history.id, 'F' "
|
||||
"as message_type, history.sender_alias, file_transfers.file_restart_id, "
|
||||
"file_transfers.file_name, file_transfers.file_path, file_transfers.file_hash, "
|
||||
"file_transfers.file_size, file_transfers.direction, file_transfers.file_state FROM "
|
||||
"history INNER JOIN file_transfers on history.file_id = file_transfers.id WHERE "
|
||||
"history.file_id IS NOT NULL");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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_new(id, message_type))");
|
||||
|
||||
// faux_offline_pending needs to be re-created to reference the new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"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("INSERT INTO faux_offline_pending_new SELECT id, "
|
||||
"required_extensions FROM faux_offline_pending");
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE faux_offline_pending");
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("ALTER TABLE faux_offline_pending_new RENAME TO faux_offline_pending");
|
||||
|
||||
// broken_messages needs to be re-created to reference the new history table
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"CREATE TABLE broken_messages_new (id INTEGER PRIMARY KEY, reason INTEGER NOT NULL DEFAULT "
|
||||
"0, FOREIGN KEY (id) REFERENCES history_new(id))");
|
||||
upgradeQueries += RawDatabase::Query(
|
||||
"INSERT INTO broken_messages_new SELECT id, reason FROM broken_messages");
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE broken_messages");
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query("ALTER TABLE broken_messages_new RENAME TO broken_messages");
|
||||
|
||||
// Everything referencing old history should now be gone
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE history");
|
||||
upgradeQueries += RawDatabase::Query("ALTER TABLE history_new RENAME TO history");
|
||||
|
||||
// Drop file transfers late since history depends on it
|
||||
upgradeQueries += RawDatabase::Query("DROP TABLE file_transfers");
|
||||
upgradeQueries += RawDatabase::Query("ALTER TABLE file_transfers_new RENAME TO file_transfers");
|
||||
|
||||
upgradeQueries += RawDatabase::Query("CREATE INDEX chat_id_idx on history (chat_id);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 7;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
bool dbSchema7to8(RawDatabase& db)
|
||||
{
|
||||
// Dummy upgrade. This upgrade does not change the schema, however on
|
||||
// version 7 if qtox saw a system message it would assert and crash. This
|
||||
// upgrade ensures that old versions of qtox do not try to load the new
|
||||
// database
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 8;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
struct BadEntry {
|
||||
BadEntry(int64_t row_, QString toxId_) :
|
||||
row{row_},
|
||||
toxId{toxId_} {}
|
||||
RowId row;
|
||||
QString toxId;
|
||||
};
|
||||
|
||||
std::vector<BadEntry> getInvalidPeers(RawDatabase& db)
|
||||
{
|
||||
std::vector<BadEntry> badPeerIds;
|
||||
db.execNow(RawDatabase::Query("SELECT id, public_key FROM peers WHERE LENGTH(public_key) != 64", [&](const QVector<QVariant>& row) {
|
||||
badPeerIds.emplace_back(BadEntry{row[0].toInt(), row[1].toString()});
|
||||
}));
|
||||
return badPeerIds;
|
||||
}
|
||||
|
||||
RowId getValidPeerRow(RawDatabase& db, const ToxPk& friendPk)
|
||||
{
|
||||
bool validPeerExists{false};
|
||||
RowId validPeerRow;
|
||||
db.execNow(RawDatabase::Query(QStringLiteral("SELECT id FROM peers WHERE public_key='%1';")
|
||||
.arg(friendPk.toString()), [&](const QVector<QVariant>& row) {
|
||||
validPeerRow = RowId{row[0].toLongLong()};
|
||||
validPeerExists = true;
|
||||
}));
|
||||
if (validPeerExists) {
|
||||
return validPeerRow;
|
||||
}
|
||||
|
||||
db.execNow(RawDatabase::Query(("SELECT id FROM peers ORDER BY id DESC LIMIT 1;"), [&](const QVector<QVariant>& row) {
|
||||
int64_t maxPeerId = row[0].toInt();
|
||||
validPeerRow = RowId{maxPeerId + 1};
|
||||
}));
|
||||
db.execNow(RawDatabase::Query(QStringLiteral("INSERT INTO peers (id, public_key) VALUES (%1, '%2');").arg(validPeerRow.get()).arg(friendPk.toString())));
|
||||
return validPeerRow;
|
||||
}
|
||||
|
||||
struct DuplicateAlias {
|
||||
DuplicateAlias(RowId goodAliasRow_, std::vector<RowId> badAliasRows_) :
|
||||
goodAliasRow{goodAliasRow_},
|
||||
badAliasRows{badAliasRows_} {}
|
||||
DuplicateAlias() {};
|
||||
RowId goodAliasRow{-1};
|
||||
std::vector<RowId> badAliasRows;
|
||||
};
|
||||
|
||||
DuplicateAlias getDuplicateAliasRows(RawDatabase& db, RowId goodPeerRow, RowId badPeerRow)
|
||||
{
|
||||
std::vector<RowId> badAliasRows;
|
||||
RowId goodAliasRow;
|
||||
bool hasGoodEntry{false};
|
||||
db.execNow(RawDatabase::Query(QStringLiteral("SELECT good.id, bad.id FROM aliases good INNER JOIN aliases bad ON good.display_name=bad.display_name WHERE good.owner=%1 AND bad.owner=%2;").arg(goodPeerRow.get()).arg(badPeerRow.get()),
|
||||
[&](const QVector<QVariant>& row) {
|
||||
hasGoodEntry = true;
|
||||
goodAliasRow = RowId{row[0].toInt()};
|
||||
badAliasRows.emplace_back(RowId{row[1].toLongLong()});
|
||||
}));
|
||||
|
||||
if (hasGoodEntry) {
|
||||
return {goodAliasRow, badAliasRows};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void mergeAndDeleteAlias(QVector<RawDatabase::Query>& upgradeQueries, RowId goodAlias, std::vector<RowId> badAliases)
|
||||
{
|
||||
for (const auto badAliasId : badAliases) {
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE text_messages SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get()));
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE file_transfers SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get()));
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM aliases WHERE id = %1;").arg(badAliasId.get()));
|
||||
}
|
||||
}
|
||||
|
||||
void mergeAndDeletePeer(QVector<RawDatabase::Query>& upgradeQueries, RowId goodPeerId, RowId badPeerId)
|
||||
{
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE aliases SET owner = %1 WHERE owner = %2").arg(goodPeerId.get()).arg(badPeerId.get()));
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE history SET chat_id = %1 WHERE chat_id = %2;").arg(goodPeerId.get()).arg(badPeerId.get()));
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM peers WHERE id = %1").arg(badPeerId.get()));
|
||||
}
|
||||
|
||||
void mergeDuplicatePeers(QVector<RawDatabase::Query>& upgradeQueries, RawDatabase& db, std::vector<BadEntry> badPeers)
|
||||
{
|
||||
for (const auto& badPeer : badPeers) {
|
||||
const RowId goodPeerId = getValidPeerRow(db, ToxPk{badPeer.toxId.left(64)});
|
||||
const auto aliasDuplicates = getDuplicateAliasRows(db, goodPeerId, badPeer.row);
|
||||
mergeAndDeleteAlias(upgradeQueries, aliasDuplicates.goodAliasRow, aliasDuplicates.badAliasRows);
|
||||
mergeAndDeletePeer(upgradeQueries, goodPeerId, badPeer.row);
|
||||
}
|
||||
}
|
||||
|
||||
bool dbSchema8to9(RawDatabase& db)
|
||||
{
|
||||
// not technically a schema update, but still a database version update based on healing invalid user data
|
||||
// we added ourself in the peers table by ToxId isntead of ToxPk. Heal this over-length entry.
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
const auto badPeers = getInvalidPeers(db);
|
||||
mergeDuplicatePeers(upgradeQueries, db, badPeers);
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 9;"));
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Upgrade the db schema
|
||||
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
|
||||
* variable and add another case to the switch statement below. Make sure to fall through on each case.
|
||||
*/
|
||||
bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
|
||||
{
|
||||
// If we're a new dB we can just make a new one and call it a day
|
||||
bool success = false;
|
||||
const bool newDb = isNewDb(db, success);
|
||||
if (!success) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newDb) {
|
||||
if (!createCurrentSchema(*db)) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we have to do upgrades from our current version to the latest version
|
||||
|
||||
int64_t databaseSchemaVersion;
|
||||
|
||||
if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
|
||||
databaseSchemaVersion = row[0].toLongLong();
|
||||
}))) {
|
||||
qCritical() << "History failed to read user_version";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion > SCHEMA_VERSION) {
|
||||
qWarning().nospace() << "Database version (" << databaseSchemaVersion
|
||||
<< ") is newer than we currently support (" << SCHEMA_VERSION
|
||||
<< "). Please upgrade qTox";
|
||||
// We don't know what future versions have done, we have to disable db access until we re-upgrade
|
||||
return false;
|
||||
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
|
||||
// No work to do
|
||||
return true;
|
||||
}
|
||||
|
||||
using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
|
||||
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
|
||||
dbSchema3to4, dbSchema4to5, dbSchema5to6,
|
||||
dbSchema6to7, dbSchema7to8, dbSchema8to9};
|
||||
|
||||
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
|
||||
assert(upgradeFns.size() == SCHEMA_VERSION);
|
||||
|
||||
for (int64_t i = databaseSchemaVersion; i < static_cast<int>(upgradeFns.size()); ++i) {
|
||||
auto const newDbVersion = i + 1;
|
||||
if (!upgradeFns[i](*db)) {
|
||||
qCritical() << "Failed to upgrade db to schema version " << newDbVersion << " aborting";
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Database upgraded incrementally to schema version " << newDbVersion;
|
||||
}
|
||||
|
||||
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion << "->"
|
||||
<< SCHEMA_VERSION << ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
MessageState getMessageState(bool isPending, bool isBroken)
|
||||
{
|
||||
assert(!(isPending && isBroken));
|
||||
|
@ -721,7 +170,7 @@ History::History(std::shared_ptr<RawDatabase> db_, Settings& settings_)
|
|||
db->execNow(
|
||||
"PRAGMA foreign_keys = ON;");
|
||||
|
||||
const auto upgradeSucceeded = dbSchemaUpgrade(db);
|
||||
const auto upgradeSucceeded = DbUpgrader::dbSchemaUpgrade(db);
|
||||
|
||||
// dbSchemaUpgrade may have put us in an invalid state
|
||||
if (!upgradeSucceeded) {
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
*/
|
||||
|
||||
#include "src/persistence/db/rawdatabase.h"
|
||||
// normally we should only test public API instead of implementation, but there's no reason to expose db schema
|
||||
// upgrade externally, and the complexity of each version upgrade benefits from being individually testable
|
||||
#include "src/persistence/history.cpp"
|
||||
#include "src/persistence/dbupgrader.h"
|
||||
|
||||
#include <QtTest/QtTest>
|
||||
#include <QString>
|
||||
|
@ -218,7 +216,7 @@ void TestDbSchema::testCreation()
|
|||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
|
||||
QVERIFY(createCurrentSchema(*db));
|
||||
QVERIFY(DbUpgrader::createCurrentSchema(*db));
|
||||
verifyDb(db, schema7);
|
||||
}
|
||||
|
||||
|
@ -226,12 +224,12 @@ void TestDbSchema::testIsNewDb()
|
|||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbTrue.db", {}, {}}};
|
||||
bool success = false;
|
||||
bool newDb = isNewDb(db, success);
|
||||
bool newDb = DbUpgrader::isNewDb(db, success);
|
||||
QVERIFY(success);
|
||||
QVERIFY(newDb == true);
|
||||
db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbFalse.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema0);
|
||||
newDb = isNewDb(db, success);
|
||||
newDb = DbUpgrader::isNewDb(db, success);
|
||||
QVERIFY(success);
|
||||
QVERIFY(newDb == false);
|
||||
}
|
||||
|
@ -240,7 +238,7 @@ void TestDbSchema::test0to1()
|
|||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test0to1.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema0);
|
||||
QVERIFY(dbSchema0to1(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema0to1(*db));
|
||||
verifyDb(db, schema1);
|
||||
}
|
||||
|
||||
|
@ -312,7 +310,7 @@ void TestDbSchema::test1to2()
|
|||
");"};
|
||||
|
||||
QVERIFY(db->execNow(queries));
|
||||
QVERIFY(dbSchema1to2(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema1to2(*db));
|
||||
verifyDb(db, schema2);
|
||||
|
||||
long brokenCount = -1;
|
||||
|
@ -375,7 +373,7 @@ void TestDbSchema::test2to3()
|
|||
" last_insert_rowid()"
|
||||
");"};
|
||||
QVERIFY(db->execNow(queries));
|
||||
QVERIFY(dbSchema2to3(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema2to3(*db));
|
||||
|
||||
long brokenCount = -1;
|
||||
RawDatabase::Query brokenCountQuery = {"SELECT COUNT(*) FROM broken_messages;", [&](const QVector<QVariant>& row) {
|
||||
|
@ -405,7 +403,7 @@ void TestDbSchema::test3to4()
|
|||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test3to4.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema3);
|
||||
QVERIFY(dbSchema3to4(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema3to4(*db));
|
||||
verifyDb(db, schema4);
|
||||
}
|
||||
|
||||
|
@ -413,7 +411,7 @@ void TestDbSchema::test4to5()
|
|||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test4to5.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema4);
|
||||
QVERIFY(dbSchema4to5(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema4to5(*db));
|
||||
verifyDb(db, schema5);
|
||||
}
|
||||
|
||||
|
@ -421,7 +419,7 @@ void TestDbSchema::test5to6()
|
|||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test5to6.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema5);
|
||||
QVERIFY(dbSchema5to6(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema5to6(*db));
|
||||
verifyDb(db, schema6);
|
||||
}
|
||||
|
||||
|
@ -432,7 +430,7 @@ void TestDbSchema::test6to7()
|
|||
db->execNow(
|
||||
"PRAGMA foreign_keys = ON;");
|
||||
createSchemaAtVersion(db, schema6);
|
||||
QVERIFY(dbSchema6to7(*db));
|
||||
QVERIFY(DbUpgrader::dbSchema6to7(*db));
|
||||
verifyDb(db, schema7);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user