From d9f1c25c7ce3689bcfeee5005abe0c497181a93f Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Wed, 27 May 2026 14:20:45 +0300 Subject: [PATCH] fix(database): make v2->v3 chatlog migration deterministic and FK-safe _migrate_to_v3 deduplicated duplicate archive_id rows with a bare-column GROUP BY, which lets SQLite keep an arbitrary row per group (in practice the older row, dropping the latest), and copied rows with SELECT *, which relies on positional column matching between the v2 and v3 schemas. - Keep MAX(id) per archive_id group ("latest insert wins") instead of GROUP BY's implementation-defined pick. - Sweep replaces_db_id / replaced_by_db_id after dedupe, NULLing references to dropped rows so the migrated table carries no dangling XEP-0308 (LMC) links (the "Broken LMC reference" condition verify_integrity flags). - List columns explicitly instead of SELECT * so the copy cannot mis-bind, and note at the schema that column changes must be mirrored here. Refs #128 --- src/database_sqlite.c | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) 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` ",