diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index 11c0c056b..34afbef15 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -26,7 +26,7 @@ #include "db/rawdatabase.h" namespace { -static constexpr int SCHEMA_VERSION = 2; +static constexpr int SCHEMA_VERSION = 3; bool createCurrentSchema(RawDatabase& db) { @@ -150,6 +150,34 @@ bool dbSchema1to2(RawDatabase& db) 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 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); +} + /** * @brief Upgrade the db schema * @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION @@ -216,6 +244,13 @@ void dbSchemaUpgrade(std::shared_ptr& db) } qDebug() << "Database upgraded incrementally to schema version 2"; //fallthrough + case 2: + if (!dbSchema2to3(*db)) { + qCritical() << "Failed to upgrade db to schema version 3, aborting"; + db.reset(); + return; + } + qDebug() << "Database upgraded incrementally to schema version 3"; // etc. default: qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion diff --git a/test/persistence/dbschema_test.cpp b/test/persistence/dbschema_test.cpp index 95fe070e3..b2bb7e955 100644 --- a/test/persistence/dbschema_test.cpp +++ b/test/persistence/dbschema_test.cpp @@ -36,6 +36,7 @@ private slots: void testIsNewDb(); void test0to1(); void test1to2(); + void test2to3(); void cleanupTestCase(); private: bool initSucess{false}; @@ -48,7 +49,8 @@ const QString testFileList[] = { "testIsNewDbTrue.db", "testIsNewDbFalse.db", "test0to1.db", - "test1to2.db" + "test1to2.db", + "test2to3.db" }; const QMap schema0 { @@ -77,6 +79,9 @@ const QMap schema2 { {"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY)"} }; +// move stuck 0-length action messages to the existing "broken_messages" table. Not a real schema upgrade. +const auto schema3 = schema2; + void TestDbSchema::initTestCase() { for (const auto& path : testFileList) { @@ -127,7 +132,7 @@ void TestDbSchema::testCreation() QVector queries; auto db = std::shared_ptr{new RawDatabase{"testCreation.db", {}, {}}}; QVERIFY(createCurrentSchema(*db)); - verifyDb(db, schema2); + verifyDb(db, schema3); } void TestDbSchema::testIsNewDb() @@ -247,5 +252,67 @@ void TestDbSchema::test1to2() QVERIFY(totalHisoryCount == 6); // all messages should still be in history. } +void TestDbSchema::test2to3() +{ + auto db = std::shared_ptr{new RawDatabase{"test2to3.db", {}, {}}}; + createSchemaAtVersion(db, schema2); + + // since we don't enforce foreign key contraints in the db, we can stick in IDs to other tables + // to avoid generating proper entries for peers and aliases tables, since they aren't actually + // relevant for the test. + + QVector queries; + // pending message, should be moved out + queries += RawDatabase::Query{ + "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (1, 1, 0, ?, 0)", + {"/me "}}; + queries += {"INSERT INTO faux_offline_pending (id) VALUES (" + " last_insert_rowid()" + ");"}; + + // non pending message with the content "/me ". Maybe it was sent by a friend using a different client. + queries += RawDatabase::Query{ + "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (2, 2, 0, ?, 2)", + {"/me "}}; + + // non pending message sent by us + queries += RawDatabase::Query{ + "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (3, 3, 0, ?, 1)", + {"a normal message"}}; + + // pending normal message sent by us + queries += RawDatabase::Query{ + "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (4, 3, 0, ?, 1)", + {"a normal faux offline message"}}; + queries += {"INSERT INTO faux_offline_pending (id) VALUES (" + " last_insert_rowid()" + ");"}; + QVERIFY(db->execNow(queries)); + QVERIFY(dbSchema2to3(*db)); + + long brokenCount = -1; + RawDatabase::Query brokenCountQuery = {"SELECT COUNT(*) FROM broken_messages;", [&](const QVector& row) { + brokenCount = row[0].toLongLong(); + }}; + QVERIFY(db->execNow(brokenCountQuery)); + QVERIFY(brokenCount == 1); + + int fauxOfflineCount = -1; + RawDatabase::Query fauxOfflineCountQuery = {"SELECT COUNT(*) FROM faux_offline_pending;", [&](const QVector& row) { + fauxOfflineCount = row[0].toLongLong(); + }}; + QVERIFY(db->execNow(fauxOfflineCountQuery)); + QVERIFY(fauxOfflineCount == 1); + + int totalHisoryCount = -1; + RawDatabase::Query totalHistoryCountQuery = {"SELECT COUNT(*) FROM history;", [&](const QVector& row) { + totalHisoryCount = row[0].toLongLong(); + }}; + QVERIFY(db->execNow(totalHistoryCountQuery)); + QVERIFY(totalHisoryCount == 4); + + verifyDb(db, schema3); +} + QTEST_GUILESS_MAIN(TestDbSchema) #include "dbschema_test.moc"