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).
This commit is contained in:
2026-05-21 12:30:33 +03:00
parent dfdca61073
commit fbdef0b9d4
2 changed files with 92 additions and 6 deletions

View File

@@ -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)
{

View File

@@ -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();