mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
feat(chatlog): Upgrade db schema to support system messages
* Resolves #6221 * System message schema designed to take enum of message base + args * New table layout required many updates to the queries executed by history * Bonus reduction of history signals/slots by issuing some file transfer insertions directly when possible
This commit is contained in:
parent
2ed7395696
commit
e9131d33aa
@ -27,7 +27,7 @@
|
|||||||
#include "src/core/toxpk.h"
|
#include "src/core/toxpk.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
static constexpr int SCHEMA_VERSION = 6;
|
static constexpr int SCHEMA_VERSION = 7;
|
||||||
|
|
||||||
bool createCurrentSchema(RawDatabase& db)
|
bool createCurrentSchema(RawDatabase& db)
|
||||||
{
|
{
|
||||||
@ -42,8 +42,16 @@ bool createCurrentSchema(RawDatabase& db)
|
|||||||
"FOREIGN KEY (owner) REFERENCES peers(id));"
|
"FOREIGN KEY (owner) REFERENCES peers(id));"
|
||||||
"CREATE TABLE history "
|
"CREATE TABLE history "
|
||||||
"(id INTEGER PRIMARY KEY, "
|
"(id INTEGER PRIMARY KEY, "
|
||||||
|
"message_type CHAR(1) NOT NULL DEFAULT 'T' CHECK (message_type in ('T','F','S')), "
|
||||||
"timestamp INTEGER NOT NULL, "
|
"timestamp INTEGER NOT NULL, "
|
||||||
"chat_id 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, "
|
"sender_alias INTEGER NOT NULL, "
|
||||||
// even though technically a message can be null for file transfer, we've opted
|
// 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
|
// to just insert an empty string when there's no content, this moderately simplifies
|
||||||
@ -52,20 +60,30 @@ bool createCurrentSchema(RawDatabase& db)
|
|||||||
// ensure that our blob vector always has the right number of fields. Better to just
|
// ensure that our blob vector always has the right number of fields. Better to just
|
||||||
// leave this as NOT NULL for now.
|
// leave this as NOT NULL for now.
|
||||||
"message BLOB NOT NULL, "
|
"message BLOB NOT NULL, "
|
||||||
"file_id INTEGER, "
|
"FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
|
||||||
"FOREIGN KEY (file_id) REFERENCES file_transfers(id), "
|
|
||||||
"FOREIGN KEY (chat_id) REFERENCES peers(id), "
|
|
||||||
"FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
|
"FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
|
||||||
"CREATE TABLE file_transfers "
|
"CREATE TABLE file_transfers "
|
||||||
"(id INTEGER PRIMARY KEY, "
|
"(id INTEGER PRIMARY KEY, "
|
||||||
"chat_id INTEGER NOT NULL, "
|
"message_type CHAR(1) NOT NULL CHECK (message_type = 'F'), "
|
||||||
|
"sender_alias INTEGER NOT NULL, "
|
||||||
"file_restart_id BLOB NOT NULL, "
|
"file_restart_id BLOB NOT NULL, "
|
||||||
"file_name BLOB NOT NULL, "
|
"file_name BLOB NOT NULL, "
|
||||||
"file_path BLOB NOT NULL, "
|
"file_path BLOB NOT NULL, "
|
||||||
"file_hash BLOB NOT NULL, "
|
"file_hash BLOB NOT NULL, "
|
||||||
"file_size INTEGER NOT NULL, "
|
"file_size INTEGER NOT NULL, "
|
||||||
"direction INTEGER NOT NULL, "
|
"direction INTEGER NOT NULL, "
|
||||||
"file_state 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, "
|
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, "
|
||||||
"required_extensions INTEGER NOT NULL DEFAULT 0, "
|
"required_extensions INTEGER NOT NULL DEFAULT 0, "
|
||||||
"FOREIGN KEY (id) REFERENCES history(id));"
|
"FOREIGN KEY (id) REFERENCES history(id));"
|
||||||
@ -290,6 +308,93 @@ bool dbSchema5to6(RawDatabase& db)
|
|||||||
return db.execNow(upgradeQueries);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Upgrade the db schema
|
* @brief Upgrade the db schema
|
||||||
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
|
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
|
||||||
@ -338,7 +443,8 @@ bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
|
|||||||
|
|
||||||
using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
|
using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
|
||||||
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
|
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
|
||||||
dbSchema3to4, dbSchema4to5, dbSchema5to6};
|
dbSchema3to4, dbSchema4to5, dbSchema5to6,
|
||||||
|
dbSchema6to7};
|
||||||
|
|
||||||
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
|
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
|
||||||
assert(upgradeFns.size() == SCHEMA_VERSION);
|
assert(upgradeFns.size() == SCHEMA_VERSION);
|
||||||
@ -390,6 +496,61 @@ RawDatabase::Query generateUpdateAlias(ToxPk const& pk, QString const& dispName)
|
|||||||
QString("INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (%1, ?);").arg(generatePeerIdString(pk)),
|
QString("INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (%1, ?);").arg(generatePeerIdString(pk)),
|
||||||
{dispName.toUtf8()});
|
{dispName.toUtf8()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate query to insert new message in database
|
||||||
|
* @param friendPk Friend publick key to save.
|
||||||
|
* @param message Message to save.
|
||||||
|
* @param sender Sender to save.
|
||||||
|
* @param time Time of message sending.
|
||||||
|
* @param isDelivered True if message was already delivered.
|
||||||
|
* @param dispName Name, which should be displayed.
|
||||||
|
* @param insertIdCallback Function, called after query execution.
|
||||||
|
*/
|
||||||
|
QVector<RawDatabase::Query>
|
||||||
|
generateNewTextMessageQueries(const ToxPk& friendPk, const QString& message, const ToxPk& sender,
|
||||||
|
const QDateTime& time, bool isDelivered, ExtensionSet extensionSet,
|
||||||
|
QString dispName, std::function<void(RowId)> insertIdCallback)
|
||||||
|
{
|
||||||
|
QVector<RawDatabase::Query> queries;
|
||||||
|
|
||||||
|
queries += generateEnsurePkInPeers(friendPk);
|
||||||
|
queries += generateEnsurePkInPeers(sender);
|
||||||
|
queries += generateUpdateAlias(sender, dispName);
|
||||||
|
queries += generateHistoryTableInsertion('T', time, friendPk);
|
||||||
|
|
||||||
|
queries += RawDatabase::Query(
|
||||||
|
QString("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);
|
||||||
|
|
||||||
|
if (!isDelivered) {
|
||||||
|
queries += RawDatabase::Query{
|
||||||
|
QString("INSERT INTO faux_offline_pending (id, required_extensions) VALUES ("
|
||||||
|
" last_insert_rowid(), %1"
|
||||||
|
");")
|
||||||
|
.arg(extensionSet.to_ulong())};
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -432,7 +593,6 @@ History::History(std::shared_ptr<RawDatabase> db_)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(this, &History::fileInsertionReady, this, &History::onFileInsertionReady);
|
|
||||||
connect(this, &History::fileInserted, this, &History::onFileInserted);
|
connect(this, &History::fileInserted, this, &History::onFileInserted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,10 +641,12 @@ void History::eraseHistory()
|
|||||||
|
|
||||||
db->execNow("DELETE FROM faux_offline_pending;"
|
db->execNow("DELETE FROM faux_offline_pending;"
|
||||||
"DELETE FROM broken_messages;"
|
"DELETE FROM broken_messages;"
|
||||||
|
"DELETE FROM text_messages;"
|
||||||
|
"DELETE FROM file_transfers;"
|
||||||
|
"DELETE FROM system_messages;"
|
||||||
"DELETE FROM history;"
|
"DELETE FROM history;"
|
||||||
"DELETE FROM aliases;"
|
"DELETE FROM aliases;"
|
||||||
"DELETE FROM peers;"
|
"DELETE FROM peers;"
|
||||||
"DELETE FROM file_transfers;"
|
|
||||||
"VACUUM;");
|
"VACUUM;");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,10 +672,21 @@ void History::removeFriendHistory(const ToxPk& friendPk)
|
|||||||
" LEFT JOIN history ON broken_messages.id = history.id "
|
" LEFT JOIN history ON broken_messages.id = history.id "
|
||||||
" WHERE chat_id=%1 "
|
" WHERE chat_id=%1 "
|
||||||
"); "
|
"); "
|
||||||
|
"DELETE FROM text_messages "
|
||||||
|
"WHERE id IN ("
|
||||||
|
" SELECT id from history "
|
||||||
|
" WHERE message_type = 'T' AND chat_id=%1);"
|
||||||
|
"DELETE FROM file_transfers "
|
||||||
|
"WHERE id IN ( "
|
||||||
|
" SELECT id from history "
|
||||||
|
" WHERE message_type = 'F' AND chat_id=%1);"
|
||||||
|
"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 history WHERE chat_id=%1; "
|
||||||
"DELETE FROM aliases WHERE owner=%1; "
|
"DELETE FROM aliases WHERE owner=%1; "
|
||||||
"DELETE FROM peers WHERE id=%1; "
|
"DELETE FROM peers WHERE id=%1; "
|
||||||
"DELETE FROM file_transfers WHERE chat_id=%1;"
|
|
||||||
"VACUUM;")
|
"VACUUM;")
|
||||||
.arg(generatePeerIdString(friendPk));
|
.arg(generatePeerIdString(friendPk));
|
||||||
|
|
||||||
@ -522,85 +695,6 @@ void History::removeFriendHistory(const ToxPk& friendPk)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate query to insert new message in database
|
|
||||||
* @param friendPk Friend publick key to save.
|
|
||||||
* @param message Message to save.
|
|
||||||
* @param sender Sender to save.
|
|
||||||
* @param time Time of message sending.
|
|
||||||
* @param isDelivered True if message was already delivered.
|
|
||||||
* @param dispName Name, which should be displayed.
|
|
||||||
* @param insertIdCallback Function, called after query execution.
|
|
||||||
*/
|
|
||||||
QVector<RawDatabase::Query>
|
|
||||||
History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
|
|
||||||
const ToxPk& sender, const QDateTime& time, bool isDelivered,
|
|
||||||
ExtensionSet extensionSet, QString dispName,
|
|
||||||
std::function<void(RowId)> insertIdCallback)
|
|
||||||
{
|
|
||||||
QVector<RawDatabase::Query> queries;
|
|
||||||
|
|
||||||
|
|
||||||
queries += generateEnsurePkInPeers(friendPk);
|
|
||||||
queries += generateEnsurePkInPeers(sender);
|
|
||||||
queries += generateUpdateAlias(sender, dispName);
|
|
||||||
|
|
||||||
queries +=
|
|
||||||
RawDatabase::Query(QString(
|
|
||||||
"INSERT INTO history (timestamp, chat_id, message, sender_alias) "
|
|
||||||
"VALUES (%1, %2, ?, ("
|
|
||||||
" SELECT id FROM aliases WHERE owner=%3 AND display_name=?)"
|
|
||||||
");")
|
|
||||||
.arg(time.toMSecsSinceEpoch())
|
|
||||||
.arg(generatePeerIdString(friendPk))
|
|
||||||
.arg(generatePeerIdString(sender)),
|
|
||||||
{message.toUtf8(), dispName.toUtf8()}, insertIdCallback);
|
|
||||||
|
|
||||||
if (!isDelivered) {
|
|
||||||
queries += RawDatabase::Query{QString("INSERT INTO faux_offline_pending (id, required_extensions) VALUES ("
|
|
||||||
" last_insert_rowid(), %1"
|
|
||||||
");")
|
|
||||||
.arg(extensionSet.to_ulong())};
|
|
||||||
}
|
|
||||||
|
|
||||||
return queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
void History::onFileInsertionReady(FileDbInsertionData data)
|
|
||||||
{
|
|
||||||
|
|
||||||
QVector<RawDatabase::Query> queries;
|
|
||||||
std::weak_ptr<History> weakThis = shared_from_this();
|
|
||||||
|
|
||||||
// Copy to pass into labmda for later
|
|
||||||
auto fileId = data.fileId;
|
|
||||||
queries +=
|
|
||||||
RawDatabase::Query(QStringLiteral(
|
|
||||||
"INSERT INTO file_transfers (chat_id, file_restart_id, "
|
|
||||||
"file_path, file_name, file_hash, file_size, direction, file_state) "
|
|
||||||
"VALUES (%1, ?, ?, ?, ?, %2, %3, %4);")
|
|
||||||
.arg(generatePeerIdString(data.friendPk))
|
|
||||||
.arg(data.size)
|
|
||||||
.arg(static_cast<int>(data.direction))
|
|
||||||
.arg(ToxFile::CANCELED),
|
|
||||||
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(),
|
|
||||||
QByteArray()},
|
|
||||||
[weakThis, fileId](RowId id) {
|
|
||||||
auto pThis = weakThis.lock();
|
|
||||||
if (pThis) {
|
|
||||||
emit pThis->fileInserted(id, fileId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
queries += RawDatabase::Query(QStringLiteral("UPDATE history "
|
|
||||||
"SET file_id = (last_insert_rowid()) "
|
|
||||||
"WHERE id = %1")
|
|
||||||
.arg(data.historyId.get()));
|
|
||||||
|
|
||||||
db->execLater(queries);
|
|
||||||
}
|
|
||||||
|
|
||||||
void History::onFileInserted(RowId dbId, QString fileId)
|
void History::onFileInserted(RowId dbId, QString fileId)
|
||||||
{
|
{
|
||||||
auto& fileInfo = fileInfos[fileId];
|
auto& fileInfo = fileInfos[fileId];
|
||||||
@ -614,6 +708,54 @@ void History::onFileInserted(RowId dbId, QString fileId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVector<RawDatabase::Query>
|
||||||
|
History::generateNewFileTransferQueries(const ToxPk& friendPk, const ToxPk& sender,
|
||||||
|
const QDateTime& time, const QString& dispName,
|
||||||
|
const FileDbInsertionData& insertionData)
|
||||||
|
{
|
||||||
|
QVector<RawDatabase::Query> queries;
|
||||||
|
|
||||||
|
queries += generateEnsurePkInPeers(friendPk);
|
||||||
|
queries += generateEnsurePkInPeers(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(
|
||||||
|
"INSERT INTO file_transfers "
|
||||||
|
" (id, message_type, sender_alias, "
|
||||||
|
" file_restart_id, file_name, file_path, "
|
||||||
|
" file_hash, file_size, direction, file_state) "
|
||||||
|
"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.toUtf8(),
|
||||||
|
insertionData.fileName.toUtf8(), insertionData.filePath.toUtf8(),
|
||||||
|
QByteArray()},
|
||||||
|
[weakThis, fileId](RowId id) {
|
||||||
|
auto pThis = weakThis.lock();
|
||||||
|
if (pThis)
|
||||||
|
emit pThis->fileInserted(id, fileId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
RawDatabase::Query History::generateFileFinished(RowId id, bool success, const QString& filePath,
|
RawDatabase::Query History::generateFileFinished(RowId id, bool success, const QString& filePath,
|
||||||
const QByteArray& fileHash)
|
const QByteArray& fileHash)
|
||||||
{
|
{
|
||||||
@ -671,17 +813,9 @@ void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId,
|
|||||||
insertionData.size = size;
|
insertionData.size = size;
|
||||||
insertionData.direction = direction;
|
insertionData.direction = direction;
|
||||||
|
|
||||||
auto insertFileTransferFn = [weakThis, insertionData](RowId messageId) {
|
auto queries = generateNewFileTransferQueries(friendPk, sender, time, dispName, insertionData);
|
||||||
auto insertionDataRw = std::move(insertionData);
|
|
||||||
|
|
||||||
insertionDataRw.historyId = messageId;
|
db->execLater(queries);
|
||||||
|
|
||||||
auto thisPtr = weakThis.lock();
|
|
||||||
if (thisPtr)
|
|
||||||
emit thisPtr->fileInsertionReady(std::move(insertionDataRw));
|
|
||||||
};
|
|
||||||
|
|
||||||
addNewMessage(friendPk, "", sender, time, true, ExtensionSet(), dispName, insertFileTransferFn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -702,7 +836,7 @@ void History::addNewMessage(const ToxPk& friendPk, const QString& message, const
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered,
|
db->execLater(generateNewTextMessageQueries(friendPk, message, sender, time, isDelivered,
|
||||||
extensionSet, dispName, insertIdCallback));
|
extensionSet, dispName, insertIdCallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,52 +908,84 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||||||
|
|
||||||
// Don't forget to update the rowCallback if you change the selected columns!
|
// Don't forget to update the rowCallback if you change the selected columns!
|
||||||
QString queryText =
|
QString queryText =
|
||||||
QString("SELECT history.id, faux_offline_pending.id, timestamp, "
|
QString(
|
||||||
"chat.public_key, aliases.display_name, sender.public_key, "
|
"SELECT history.id, history.message_type, history.timestamp, faux_offline_pending.id, "
|
||||||
"message, file_transfers.file_restart_id, "
|
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
|
||||||
"file_transfers.file_path, file_transfers.file_name, "
|
" file_restart_id, file_name, file_path, file_size, file_transfers.direction, "
|
||||||
"file_transfers.file_size, file_transfers.direction, "
|
" file_state, peers.public_key as sender_key, aliases.display_name, "
|
||||||
"file_transfers.file_state, broken_messages.id, "
|
" system_messages.system_message_type, system_messages.arg1, system_messages.arg2, "
|
||||||
"faux_offline_pending.required_extensions FROM history "
|
" system_messages.arg3, system_messages.arg4 "
|
||||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
"FROM history "
|
||||||
"JOIN peers chat ON history.chat_id = chat.id "
|
"LEFT JOIN text_messages ON history.id = text_messages.id "
|
||||||
"JOIN aliases ON sender_alias = aliases.id "
|
"LEFT JOIN file_transfers ON history.id = file_transfers.id "
|
||||||
"JOIN peers sender ON aliases.owner = sender.id "
|
"LEFT JOIN system_messages ON system_messages.id == history.id "
|
||||||
"LEFT JOIN file_transfers ON history.file_id = file_transfers.id "
|
"LEFT JOIN aliases ON text_messages.sender_alias = aliases.id OR "
|
||||||
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
|
"file_transfers.sender_alias = aliases.id "
|
||||||
"WHERE chat.public_key='%1' "
|
"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;")
|
"LIMIT %2 OFFSET %3;")
|
||||||
.arg(friendPk.toString())
|
.arg(generatePeerIdString(friendPk))
|
||||||
.arg(lastIdx - firstIdx)
|
.arg(lastIdx - firstIdx)
|
||||||
.arg(firstIdx);
|
.arg(firstIdx);
|
||||||
|
|
||||||
auto rowCallback = [&messages](const QVector<QVariant>& row) {
|
auto rowCallback = [&friendPk, &messages](const QVector<QVariant>& row) {
|
||||||
// dispName and message could have null bytes, QString::fromUtf8
|
// If the select statement is changed please update these constants
|
||||||
// truncates on null bytes so we strip them
|
constexpr auto messageOffset = 6;
|
||||||
auto id = RowId{row[0].toLongLong()};
|
constexpr auto fileOffset = 7;
|
||||||
auto isPending = !row[1].isNull();
|
constexpr auto senderOffset = 13;
|
||||||
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
|
constexpr auto systemOffset = 15;
|
||||||
auto friend_key = row[3].toString();
|
|
||||||
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
|
|
||||||
auto sender_key = row[5].toString();
|
|
||||||
auto isBroken = !row[13].isNull();
|
|
||||||
auto requiredExtensions = ExtensionSet(row[14].toLongLong());
|
|
||||||
|
|
||||||
MessageState messageState = getMessageState(isPending, isBroken);
|
auto it = row.begin();
|
||||||
|
|
||||||
if (row[7].isNull()) {
|
const auto id = RowId{(*it++).toLongLong()};
|
||||||
messages += {id, messageState, requiredExtensions, timestamp, friend_key,
|
const auto messageType = (*it++).toString();
|
||||||
display_name, sender_key, row[6].toString()};
|
const auto timestamp = QDateTime::fromMSecsSinceEpoch((*it++).toLongLong());
|
||||||
} else {
|
const auto isPending = !(*it++).isNull();
|
||||||
|
// If NULL this should just reutrn 0 which is an empty extension set, good enough for now
|
||||||
|
const auto requiredExtensions = ExtensionSet((*it++).toLongLong());
|
||||||
|
const auto isBroken = !(*it++).isNull();
|
||||||
|
const auto messageState = getMessageState(isPending, isBroken);
|
||||||
|
|
||||||
|
// Intentionally arrange query so message types are at the end so we don't have to think
|
||||||
|
// about the iterator jumping around after handling the different types.
|
||||||
|
assert(messageType.size() == 1);
|
||||||
|
switch (messageType[0].toLatin1()) {
|
||||||
|
case 'T': {
|
||||||
|
it = std::next(row.begin(), messageOffset);
|
||||||
|
assert(!it->isNull());
|
||||||
|
const auto messageContent = (*it++).toString();
|
||||||
|
it = std::next(row.begin(), senderOffset);
|
||||||
|
const auto senderKey = (*it++).toString();
|
||||||
|
const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
|
||||||
|
messages += HistMessage(id, messageState, requiredExtensions, timestamp,
|
||||||
|
friendPk.toString(), senderName, senderKey, messageContent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'F': {
|
||||||
|
it = std::next(row.begin(), fileOffset);
|
||||||
|
assert(!it->isNull());
|
||||||
ToxFile file;
|
ToxFile file;
|
||||||
file.fileKind = TOX_FILE_KIND_DATA;
|
file.fileKind = TOX_FILE_KIND_DATA;
|
||||||
file.resumeFileId = row[7].toString().toUtf8();
|
file.resumeFileId = (*it++).toString().toUtf8();
|
||||||
file.filePath = row[8].toString();
|
file.fileName = (*it++).toString();
|
||||||
file.fileName = row[9].toString();
|
file.filePath = (*it++).toString();
|
||||||
file.filesize = row[10].toLongLong();
|
file.filesize = (*it++).toLongLong();
|
||||||
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
|
file.direction = static_cast<ToxFile::FileDirection>((*it++).toLongLong());
|
||||||
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
|
file.status = static_cast<ToxFile::FileStatus>((*it++).toLongLong());
|
||||||
messages += {id, messageState, timestamp, friend_key, display_name, sender_key, file};
|
it = std::next(row.begin(), senderOffset);
|
||||||
|
const auto senderKey = (*it++).toString();
|
||||||
|
const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
|
||||||
|
messages += HistMessage(id, messageState, timestamp, friendPk.toString(), senderName,
|
||||||
|
senderKey, file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
case 'S':
|
||||||
|
// System messages not yet supported
|
||||||
|
assert(false);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -835,35 +1001,37 @@ QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto queryText =
|
auto queryText =
|
||||||
QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, "
|
QString(
|
||||||
"aliases.display_name, sender.public_key, message, broken_messages.id, "
|
"SELECT history.id, history.timestamp, faux_offline_pending.id, "
|
||||||
"faux_offline_pending.required_extensions "
|
" faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
|
||||||
|
" peers.public_key as sender_key, aliases.display_name "
|
||||||
"FROM history "
|
"FROM history "
|
||||||
"JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
"JOIN text_messages ON history.id = text_messages.id "
|
||||||
"JOIN peers chat on history.chat_id = chat.id "
|
"JOIN aliases ON text_messages.sender_alias = aliases.id "
|
||||||
"JOIN aliases on sender_alias = aliases.id "
|
"JOIN peers ON aliases.owner = peers.id "
|
||||||
"JOIN peers sender on aliases.owner = sender.id "
|
"JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
|
||||||
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
|
"LEFT JOIN broken_messages ON broken_messages.id = history.id "
|
||||||
"WHERE chat.public_key='%1';")
|
"WHERE history.chat_id = %1 AND history.message_type = 'T';")
|
||||||
.arg(friendPk.toString());
|
.arg(generatePeerIdString(friendPk));
|
||||||
|
|
||||||
QList<History::HistMessage> ret;
|
QList<History::HistMessage> ret;
|
||||||
auto rowCallback = [&ret](const QVector<QVariant>& row) {
|
auto rowCallback = [&friendPk, &ret](const QVector<QVariant>& row) {
|
||||||
|
auto it = row.begin();
|
||||||
// dispName and message could have null bytes, QString::fromUtf8
|
// dispName and message could have null bytes, QString::fromUtf8
|
||||||
// truncates on null bytes so we strip them
|
// truncates on null bytes so we strip them
|
||||||
auto id = RowId{row[0].toLongLong()};
|
auto id = RowId{(*it++).toLongLong()};
|
||||||
auto isPending = !row[1].isNull();
|
auto timestamp = QDateTime::fromMSecsSinceEpoch((*it++).toLongLong());
|
||||||
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
|
auto isPending = !(*it++).isNull();
|
||||||
auto friend_key = row[3].toString();
|
auto extensionSet = ExtensionSet((*it++).toLongLong());
|
||||||
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
|
auto isBroken = !(*it++).isNull();
|
||||||
auto sender_key = row[5].toString();
|
auto messageContent = (*it++).toString();
|
||||||
auto isBroken = !row[7].isNull();
|
auto senderKey = (*it++).toString();
|
||||||
auto extensionSet = ExtensionSet(row[8].toLongLong());
|
auto displayName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
|
||||||
|
|
||||||
MessageState messageState = getMessageState(isPending, isBroken);
|
MessageState messageState = getMessageState(isPending, isBroken);
|
||||||
|
|
||||||
ret +=
|
ret += {id, messageState, extensionSet, timestamp, friendPk.toString(),
|
||||||
{id, messageState, extensionSet, timestamp, friend_key, display_name, sender_key, row[6].toString()};
|
displayName, senderKey, messageContent};
|
||||||
};
|
};
|
||||||
|
|
||||||
db->execNow({queryText, rowCallback});
|
db->execNow({queryText, rowCallback});
|
||||||
@ -897,24 +1065,24 @@ QDateTime History::getDateWhereFindPhrase(const ToxPk& friendPk, const QDateTime
|
|||||||
|
|
||||||
switch (parameter.filter) {
|
switch (parameter.filter) {
|
||||||
case FilterSearch::Register:
|
case FilterSearch::Register:
|
||||||
message = QStringLiteral("message LIKE '%%1%'").arg(phrase);
|
message = QStringLiteral("text_messages.message LIKE '%%1%'").arg(phrase);
|
||||||
break;
|
break;
|
||||||
case FilterSearch::WordsOnly:
|
case FilterSearch::WordsOnly:
|
||||||
message = QStringLiteral("message REGEXP '%1'")
|
message = QStringLiteral("text_messages.message REGEXP '%1'")
|
||||||
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
||||||
break;
|
break;
|
||||||
case FilterSearch::RegisterAndWordsOnly:
|
case FilterSearch::RegisterAndWordsOnly:
|
||||||
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')")
|
message = QStringLiteral("REGEXPSENSITIVE(text_messages.message, '%1')")
|
||||||
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
||||||
break;
|
break;
|
||||||
case FilterSearch::Regular:
|
case FilterSearch::Regular:
|
||||||
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
|
message = QStringLiteral("text_messages.message REGEXP '%1'").arg(phrase);
|
||||||
break;
|
break;
|
||||||
case FilterSearch::RegisterAndRegular:
|
case FilterSearch::RegisterAndRegular:
|
||||||
message = QStringLiteral("REGEXPSENSITIVE(message '%1')").arg(phrase);
|
message = QStringLiteral("REGEXPSENSITIVE(text_messages.message '%1')").arg(phrase);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
message = QStringLiteral("LOWER(message) LIKE '%%1%'").arg(phrase.toLower());
|
message = QStringLiteral("LOWER(text_messages.message) LIKE '%%1%'").arg(phrase.toLower());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -950,8 +1118,8 @@ QDateTime History::getDateWhereFindPhrase(const ToxPk& friendPk, const QDateTime
|
|||||||
QString queryText =
|
QString queryText =
|
||||||
QStringLiteral("SELECT timestamp "
|
QStringLiteral("SELECT timestamp "
|
||||||
"FROM history "
|
"FROM history "
|
||||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
|
||||||
"JOIN peers chat ON chat_id = chat.id "
|
"JOIN peers chat ON chat_id = chat.id "
|
||||||
|
"JOIN text_messages ON history.id = text_messages.id "
|
||||||
"WHERE chat.public_key='%1' "
|
"WHERE chat.public_key='%1' "
|
||||||
"AND %2 "
|
"AND %2 "
|
||||||
"%3")
|
"%3")
|
||||||
|
@ -190,24 +190,22 @@ public:
|
|||||||
void markAsDelivered(RowId messageId);
|
void markAsDelivered(RowId messageId);
|
||||||
void markAsBroken(RowId messageId, BrokenMessageReason reason);
|
void markAsBroken(RowId messageId, BrokenMessageReason reason);
|
||||||
|
|
||||||
protected:
|
|
||||||
QVector<RawDatabase::Query>
|
|
||||||
generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
|
|
||||||
const ToxPk& sender, const QDateTime& time, bool isDelivered,
|
|
||||||
ExtensionSet extensionSet, QString dispName, std::function<void(RowId)> insertIdCallback = {});
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void fileInsertionReady(FileDbInsertionData data);
|
|
||||||
void fileInserted(RowId dbId, QString fileId);
|
void fileInserted(RowId dbId, QString fileId);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onFileInsertionReady(FileDbInsertionData data);
|
|
||||||
void onFileInserted(RowId dbId, QString fileId);
|
void onFileInserted(RowId dbId, QString fileId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QVector<RawDatabase::Query>
|
||||||
|
generateNewFileTransferQueries(const ToxPk& friendPk, const ToxPk& sender, const QDateTime& time,
|
||||||
|
const QString& dispName, const FileDbInsertionData& insertionData);
|
||||||
bool historyAccessBlocked();
|
bool historyAccessBlocked();
|
||||||
static RawDatabase::Query generateFileFinished(RowId fileId, bool success,
|
static RawDatabase::Query generateFileFinished(RowId fileId, bool success,
|
||||||
const QString& filePath, const QByteArray& fileHash);
|
const QString& filePath, const QByteArray& fileHash);
|
||||||
|
|
||||||
|
int64_t getPeerId(ToxPk const& pk);
|
||||||
|
|
||||||
std::shared_ptr<RawDatabase> db;
|
std::shared_ptr<RawDatabase> db;
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,24 +52,19 @@ private slots:
|
|||||||
void test3to4();
|
void test3to4();
|
||||||
void test4to5();
|
void test4to5();
|
||||||
void test5to6();
|
void test5to6();
|
||||||
void cleanupTestCase();
|
void test6to7();
|
||||||
|
void cleanupTestCase() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initSucess{false};
|
bool initSucess{false};
|
||||||
void createSchemaAtVersion(std::shared_ptr<RawDatabase>, const std::vector<SqliteMasterEntry>& schema);
|
void createSchemaAtVersion(std::shared_ptr<RawDatabase>, const std::vector<SqliteMasterEntry>& schema);
|
||||||
void verifyDb(std::shared_ptr<RawDatabase> db, const std::vector<SqliteMasterEntry>& expectedSql);
|
void verifyDb(std::shared_ptr<RawDatabase> db, const std::vector<SqliteMasterEntry>& expectedSql);
|
||||||
};
|
};
|
||||||
|
|
||||||
const QString testFileList[] = {
|
const QString testFileList[] = {"testCreation.db", "testIsNewDbTrue.db", "testIsNewDbFalse.db",
|
||||||
"testCreation.db",
|
"test0to1.db", "test1to2.db", "test2to3.db",
|
||||||
"testIsNewDbTrue.db",
|
"test3to4.db", "test4to5.db", "test5to6.db",
|
||||||
"testIsNewDbFalse.db",
|
"test6to7.db"};
|
||||||
"test0to1.db",
|
|
||||||
"test1to2.db",
|
|
||||||
"test2to3.db",
|
|
||||||
"test3to4.db",
|
|
||||||
"test4to5.db",
|
|
||||||
"test5to6.db"
|
|
||||||
};
|
|
||||||
|
|
||||||
// db schemas can be select with "SELECT name, sql FROM sqlite_master;" on the database.
|
// db schemas can be select with "SELECT name, sql FROM sqlite_master;" on the database.
|
||||||
|
|
||||||
@ -135,6 +130,37 @@ const std::vector<SqliteMasterEntry> schema6 {
|
|||||||
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
|
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const std::vector<SqliteMasterEntry> 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",
|
||||||
|
"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 peers(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))"},
|
||||||
|
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT 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 TestDbSchema::initTestCase()
|
void TestDbSchema::initTestCase()
|
||||||
{
|
{
|
||||||
for (const auto& path : testFileList) {
|
for (const auto& path : testFileList) {
|
||||||
@ -143,7 +169,7 @@ void TestDbSchema::initTestCase()
|
|||||||
initSucess = true;
|
initSucess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestDbSchema::cleanupTestCase()
|
void TestDbSchema::cleanupTestCase() const
|
||||||
{
|
{
|
||||||
if (!initSucess) {
|
if (!initSucess) {
|
||||||
qWarning() << "init failed, skipping cleanup to avoid loss of data";
|
qWarning() << "init failed, skipping cleanup to avoid loss of data";
|
||||||
@ -189,7 +215,7 @@ void TestDbSchema::testCreation()
|
|||||||
QVector<RawDatabase::Query> queries;
|
QVector<RawDatabase::Query> queries;
|
||||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
|
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
|
||||||
QVERIFY(createCurrentSchema(*db));
|
QVERIFY(createCurrentSchema(*db));
|
||||||
verifyDb(db, schema6);
|
verifyDb(db, schema7);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestDbSchema::testIsNewDb()
|
void TestDbSchema::testIsNewDb()
|
||||||
@ -395,5 +421,16 @@ void TestDbSchema::test5to6()
|
|||||||
verifyDb(db, schema6);
|
verifyDb(db, schema6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDbSchema::test6to7()
|
||||||
|
{
|
||||||
|
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test6to7.db", {}, {}}};
|
||||||
|
// foreign_keys are enabled by History constructor and required for this upgrade to work on older sqlite versions
|
||||||
|
db->execNow(
|
||||||
|
"PRAGMA foreign_keys = ON;");
|
||||||
|
createSchemaAtVersion(db, schema6);
|
||||||
|
QVERIFY(dbSchema6to7(*db));
|
||||||
|
verifyDb(db, schema7);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestDbSchema)
|
QTEST_GUILESS_MAIN(TestDbSchema)
|
||||||
#include "dbschema_test.moc"
|
#include "dbschema_test.moc"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user