diff --git a/src/database_sqlite.c b/src/database_sqlite.c index 9af82bc4..74002fca 100644 --- a/src/database_sqlite.c +++ b/src/database_sqlite.c @@ -178,6 +178,9 @@ _sqlite_init(ProfAccount* account) // replaced_by_db_id Inverse FK: id of the most recent correction. // Maintained by the AFTER INSERT trigger below so // readers can follow the chain forward in O(1). + // + // NOTE: any change to these columns (add/remove/reorder) must be mirrored + // in `_migrate_to_v3`, which copies rows via an explicit column list. const char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` (" "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " "`from_jid` TEXT NOT NULL, " @@ -865,11 +868,37 @@ _migrate_to_v3(void) "`replaces_db_id` INTEGER, " "`replaced_by_db_id` INTEGER)", + // Copy rows into the deduped table. Columns are listed explicitly + // (instead of `SELECT *`) so the migration cannot silently mis-bind + // should the v2/v3 column order ever diverge — keep this list in sync + // with the `ChatLogs_v3_migration` schema above and the live `ChatLogs` + // schema. For duplicate `archive_id` groups (MAM rebroadcasts) keep the + // highest `id` deterministically ("latest insert wins") rather than + // GROUP BY's implementation-defined arbitrary pick. "INSERT INTO `ChatLogs_v3_migration` " - "SELECT * FROM `ChatLogs` " - "WHERE `archive_id` IS NULL " + "(`id`, `from_jid`, `to_jid`, `from_resource`, `to_resource`, `message`, " + "`timestamp`, `type`, `stanza_id`, `archive_id`, `encryption`, " + "`marked_read`, `replace_id`, `replaces_db_id`, `replaced_by_db_id`) " + "SELECT `id`, `from_jid`, `to_jid`, `from_resource`, `to_resource`, `message`, " + "`timestamp`, `type`, `stanza_id`, `archive_id`, `encryption`, " + "`marked_read`, `replace_id`, `replaces_db_id`, `replaced_by_db_id` " + "FROM `ChatLogs` WHERE `archive_id` IS NULL " "UNION ALL " - "SELECT * FROM (SELECT * FROM `ChatLogs` WHERE `archive_id` IS NOT NULL GROUP BY `archive_id`)", + "SELECT `id`, `from_jid`, `to_jid`, `from_resource`, `to_resource`, `message`, " + "`timestamp`, `type`, `stanza_id`, `archive_id`, `encryption`, " + "`marked_read`, `replace_id`, `replaces_db_id`, `replaced_by_db_id` " + "FROM `ChatLogs` WHERE `archive_id` IS NOT NULL " + "AND `id` IN (SELECT MAX(`id`) FROM `ChatLogs` WHERE `archive_id` IS NOT NULL GROUP BY `archive_id`)", + + // Dropping duplicate rows above can leave the surviving rows' correction + // back-links pointing at ids that no longer exist. NULL them out so the + // XEP-0308 (LMC) chain carries no dangling local foreign keys. + "UPDATE `ChatLogs_v3_migration` SET `replaces_db_id` = NULL " + "WHERE `replaces_db_id` IS NOT NULL " + "AND `replaces_db_id` NOT IN (SELECT `id` FROM `ChatLogs_v3_migration`)", + "UPDATE `ChatLogs_v3_migration` SET `replaced_by_db_id` = NULL " + "WHERE `replaced_by_db_id` IS NOT NULL " + "AND `replaced_by_db_id` NOT IN (SELECT `id` FROM `ChatLogs_v3_migration`)", "DROP TABLE `ChatLogs` ", "ALTER TABLE `ChatLogs_v3_migration` RENAME TO `ChatLogs` ",