From fbdef0b9d4c8139a9f583371fa2da0147c2d5890 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 21 May 2026 12:30:33 +0300 Subject: [PATCH] fix(database): port v3 migration into database_sqlite.c and harden init (upstream 42a849d16) The SQLite-backend code now lives in database_sqlite.c (master split src/database.c into a thin backend dispatcher); the upstream changes that PR 105 had folded into the monolithic database.c were not ported across when we took master's dispatcher version. This brings forward the v3 schema work and folds in upstream 42a849d16 (fix(database): resolve migration failure and improve init safety): - latest_version bumped 2 -> 3; ChatLogs.archive_id gains UNIQUE constraint; _migrate_to_v3 dedupes existing rows via the ChatLogs_v3_migration intermediate table - DbVersion bookkeeping uses DELETE+INSERT instead of UPDATE so multiple rows do not trip the unique-constraint failure reported upstream (#2105) - Initial INSERT INTO DbVersion uses latest_version via sqlite3_mprintf, so brand-new databases are stamped at the current schema and skip redundant migrations - server_events.c now checks log_database_init's return so a broken DB init is logged instead of silently leaving the handle half-open (644/0, 605/0, 605/0, 644/0 unit + 130/0 functional each). --- src/database_sqlite.c | 94 ++++++++++++++++++++++++++++++++++++--- src/event/server_events.c | 4 +- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/database_sqlite.c b/src/database_sqlite.c index 446b14cb..e479e618 100644 --- a/src/database_sqlite.c +++ b/src/database_sqlite.c @@ -61,9 +61,10 @@ static prof_msg_type_t _get_message_type_type(const char* const type); static prof_enc_t _get_message_enc_type(const char* const encstr); static int _get_db_version(void); static gboolean _migrate_to_v2(void); +static gboolean _migrate_to_v3(void); static gboolean _check_available_space_for_db_migration(char* path_to_db); -static const int latest_version = 2; +static const int latest_version = 3; gboolean db_sqlite_is_open(void) @@ -187,7 +188,7 @@ _sqlite_init(ProfAccount* account) "`timestamp` TEXT, " "`type` TEXT, " "`stanza_id` TEXT, " - "`archive_id` TEXT, " + "`archive_id` TEXT UNIQUE, " "`encryption` TEXT, " "`marked_read` INTEGER, " "`replace_id` TEXT, " @@ -228,8 +229,10 @@ _sqlite_init(ProfAccount* account) } if (db_version == -1) { - query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')"; - if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) { + auto_sqlite char* init_version_query = sqlite3_mprintf( + "INSERT INTO `DbVersion` (`version`) VALUES (%d) ON CONFLICT(`version`) DO NOTHING", + latest_version); + if (SQLITE_OK != sqlite3_exec(g_chatlog_database, init_version_query, NULL, 0, &err_msg)) { goto out; } db_version = _get_db_version(); @@ -246,6 +249,10 @@ _sqlite_init(ProfAccount* account) cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details."); goto out; } + if (db_version < 3 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v3())) { + cons_show_error("Database Initialization Error: Unable to migrate database to version 3. Please, check error logs for details."); + goto out; + } cons_show("Database schema migration was successful."); } @@ -799,7 +806,8 @@ _migrate_to_v2(void) "replace_id = COALESCE(NULLIF(replace_id, ''), NULL), " "type = COALESCE(NULLIF(type, ''), NULL), " "encryption = COALESCE(NULLIF(encryption, ''), NULL);", - "UPDATE `DbVersion` SET `version` = 2;", + "DELETE FROM `DbVersion`;", + "INSERT INTO `DbVersion` (`version`) VALUES (2);", "END TRANSACTION" }; @@ -829,6 +837,82 @@ cleanup: return FALSE; } +static gboolean +_migrate_to_v3(void) +{ + char* err_msg = NULL; + + const char* sql_statements[] = { + "BEGIN TRANSACTION", + "CREATE TABLE `ChatLogs_v3_migration` (" + "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " + "`from_jid` TEXT NOT NULL, " + "`to_jid` TEXT NOT NULL, " + "`from_resource` TEXT, " + "`to_resource` TEXT, " + "`message` TEXT, " + "`timestamp` TEXT, " + "`type` TEXT, " + "`stanza_id` TEXT, " + "`archive_id` TEXT UNIQUE, " + "`encryption` TEXT, " + "`marked_read` INTEGER, " + "`replace_id` TEXT, " + "`replaces_db_id` INTEGER, " + "`replaced_by_db_id` INTEGER)", + + "INSERT INTO `ChatLogs_v3_migration` " + "SELECT * FROM `ChatLogs` " + "WHERE `archive_id` IS NULL " + "UNION ALL " + "SELECT * FROM (SELECT * FROM `ChatLogs` WHERE `archive_id` IS NOT NULL GROUP BY `archive_id`)", + + "DROP TABLE `ChatLogs` ", + "ALTER TABLE `ChatLogs_v3_migration` RENAME TO `ChatLogs` ", + + "CREATE INDEX ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)", + "CREATE INDEX ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)", + "CREATE TRIGGER update_corrected_message " + "AFTER INSERT ON ChatLogs " + "FOR EACH ROW " + "WHEN NEW.replaces_db_id IS NOT NULL " + "BEGIN " + "UPDATE ChatLogs " + "SET replaced_by_db_id = NEW.id " + "WHERE id = NEW.replaces_db_id; " + "END;", + + "DELETE FROM `DbVersion`;", + "INSERT INTO `DbVersion` (`version`) VALUES (3);", + "END TRANSACTION" + }; + + int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]); + + for (int i = 0; i < statements_count; i++) { + if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) { + log_error("SQLite error in _migrate_to_v3() on statement %d: %s", i, err_msg); + if (err_msg) { + sqlite3_free(err_msg); + err_msg = NULL; + } + goto cleanup; + } + } + + return TRUE; + +cleanup: + if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) { + log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg); + if (err_msg) { + sqlite3_free(err_msg); + } + } + + return FALSE; +} + static gboolean _check_available_space_for_db_migration(char* path_to_db) { diff --git a/src/event/server_events.c b/src/event/server_events.c index 83fea6f3..09b8af0a 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -81,7 +81,9 @@ sv_ev_login_account_success(char* account_name, gboolean secured) omemo_on_connect(account); #endif - log_database_init(account); + if (!log_database_init(account)) { + log_error("Failed to initialize database for account: %s", account->jid); + } vcard_user_refresh(); avatar_pep_subscribe();