no-DB mode implementation #94

Manually merged
jabber.developer merged 34 commits from feat/no-db-mode into master 2026-05-05 19:32:42 +00:00
34 changed files with 8579 additions and 711 deletions
Showing only changes of commit 7ba7e53291 - Show all commits

View File

@@ -6729,6 +6729,54 @@ cmd_logging(ProfWin* window, const char* const command, gchar** args)
return TRUE; return TRUE;
} }
static void
_show_integrity_issues(GSList* issues)
{
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
jabber.developer2 marked this conversation as resolved Outdated

It future, this requirement should be removed as it doesn't make much sense. Offline capabilities are really lacking. But I see why it exists (local SQLite is getting disconnected on disconnect).

Feel free to click "resolve" on this comment.

It future, this requirement should be removed as it doesn't make much sense. Offline capabilities are really lacking. But I see why it exists (local SQLite is getting disconnected on disconnect). Feel free to click "resolve" on this comment.
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
jabber.developer2 marked this conversation as resolved Outdated

Nice nesting: everything is flat. Easy to read, easy to maintain.

Resolveable.

Nice nesting: everything is flat. Easy to read, easy to maintain. Resolveable.
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
}
gboolean gboolean
cmd_history(ProfWin* window, const char* const command, gchar** args) cmd_history(ProfWin* window, const char* const command, gchar** args)
{ {
@@ -6773,50 +6821,7 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
cons_show("Verifying history integrity..."); cons_show("Verifying history integrity...");
GSList* issues = log_database_verify_integrity(contact_jid); GSList* issues = log_database_verify_integrity(contact_jid);
_show_integrity_issues(issues);
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
return TRUE; return TRUE;
} }

View File

@@ -195,46 +195,11 @@ typedef struct
int skipped; // counter int skipped; // counter
} export_ctx_t; } export_ctx_t;
int // Export a single contact's messages from SQLite to flatfile.
log_database_export_to_flatfile(const gchar* const contact_jid) // Returns the number of newly exported messages, or -1 on error/skip.
static int
_export_one_contact(const char* contact)
{ {
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Export failed: not connected.");
return -1;
}
if (!g_flatfile_account_jid) {
// Flatfile backend not initialized — init it temporarily
// We need g_flatfile_account_jid for path construction
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
// Get the SQLite backend
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Export failed: SQLite backend not available.");
return -1;
}
// Determine contact list
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = db_sqlite_list_contacts();
if (!contacts) {
cons_show("No contacts found in SQLite database.");
return 0;
}
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
}
int total_exported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
// 1. Read existing flatfile lines for dedup // 1. Read existing flatfile lines for dedup
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
@@ -253,7 +218,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
log_debug("export: no SQLite messages for %s", contact); log_debug("export: no SQLite messages for %s", contact);
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
continue; return -1;
} }
// 3. Merge: write existing flatfile lines + new SQLite lines to temp file // 3. Merge: write existing flatfile lines + new SQLite lines to temp file
@@ -262,7 +227,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
// Ensure directory // Ensure directory
@@ -286,7 +251,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
FILE* fp = fdopen(tmp_fd, "w"); FILE* fp = fdopen(tmp_fd, "w");
if (!fp) { if (!fp) {
@@ -296,7 +261,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
// Lock the temp file // Lock the temp file
@@ -397,6 +362,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
fclose(fp); // also releases flock fclose(fp); // also releases flock
// Atomic rename // Atomic rename
int result = -1;
if (rename(tmp_path, log_path) != 0) { if (rename(tmp_path, log_path) != 0) {
log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno)); log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno));
cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno)); cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno));
@@ -404,12 +370,57 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
} else { } else {
cons_show("Exported %s: %d new, %d skipped (already existed: %d)", cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
contact, contact_exported, contact_skipped, existing_count); contact, contact_exported, contact_skipped, existing_count);
total_exported += contact_exported; result = contact_exported;
} }
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return result;
}
int
log_database_export_to_flatfile(const gchar* const contact_jid)
{
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Export failed: not connected.");
return -1;
}
jabber.developer marked this conversation as resolved Outdated

We can't just print Exported %s... to user after this case. It's a error and should be handled as such.

We can't just print `Exported %s`... to user after this case. It's a error and should be handled as such.

Corrected

Corrected
if (!g_flatfile_account_jid) {
// Flatfile backend not initialized — init it temporarily
// We need g_flatfile_account_jid for path construction
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
// Get the SQLite backend
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Export failed: SQLite backend not available.");
return -1;
}
// Determine contact list
jabber.developer marked this conversation as resolved Outdated

nit: This method is quite fat. In some sense, it's needed for coherency, but I might've overlooked some potential issues with early returns. If possible, I suggest extracting at least body of the cycle.

nit: This method is quite fat. In some sense, it's needed for coherency, but I might've overlooked some potential issues with early returns. If possible, I suggest extracting at least body of the cycle.

Corrected

Corrected
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = db_sqlite_list_contacts();
if (!contacts) {
cons_show("No contacts found in SQLite database.");
return 0;
}
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
}
int total_exported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
int exported = _export_one_contact(contact);
if (exported > 0)
total_exported += exported;
} }
g_slist_free_full(contacts, g_free); g_slist_free_full(contacts, g_free);

View File

@@ -163,23 +163,12 @@ _ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
g_date_time_unref(dt); g_date_time_unref(dt);
jabber.developer marked this conversation as resolved Outdated

Please, extract bom-check to a different method. it is violates DRY and SRP. database_flatfile_validate.c:123

Please, extract bom-check to a different method. it is violates DRY and SRP. `database_flatfile_validate.c:123`

Corrected

Corrected
} }
static gboolean // Scan lines from an already-positioned file pointer, caching IDs and
_ff_state_build(ff_contact_state_t* state) // building index entries. Returns the number of messages scanned.
static size_t
_ff_scan_lines(ff_contact_state_t* state, FILE* fp)
{ {
FILE* fp = fopen(state->filepath, "r"); size_t count = 0;
if (!fp)
return FALSE;
state->bom_len = ff_skip_bom(fp);
state->n_entries = 0;
state->total_lines = 0;
size_t msg_count = 0;
// Clear ID caches for full rebuild
g_hash_table_remove_all(state->archive_ids);
g_hash_table_remove_all(state->stanza_senders);
while (1) { while (1) {
off_t pos = ftell(fp); off_t pos = ftell(fp);
gboolean truncated = FALSE; gboolean truncated = FALSE;
@@ -197,16 +186,34 @@ _ff_state_build(ff_contact_state_t* state)
} }
state->total_lines++; state->total_lines++;
// Extract IDs for O(1) dedup/LMC cache
_ff_cache_line_ids(state, buf); _ff_cache_line_ids(state, buf);
if (msg_count % FF_INDEX_STEP == 0) { if (count % FF_INDEX_STEP == 0) {
_ff_maybe_index_line(state, buf, pos); _ff_maybe_index_line(state, buf, pos);
} }
msg_count++; count++;
free(buf); free(buf);
} }
return count;
}
jabber.developer marked this conversation as resolved Outdated

please add comment explaining purpose

please add comment explaining purpose

Corrected

Corrected
static gboolean
_ff_state_build(ff_contact_state_t* state)
{
FILE* fp = fopen(state->filepath, "r");
if (!fp)
jabber.developer marked this conversation as resolved Outdated

Are !dt and !space cases ok to be ignored?

Are `!dt` and `!space` cases ok to be ignored?

Postponed — discussion. Current behaviour is "skip silently" (lines outside index step are not indexed); this is by design. No specific change requested.

Postponed — discussion. Current behaviour is "skip silently" (lines outside index step are not indexed); this is by design. No specific change requested.
return FALSE;
jabber.developer marked this conversation as resolved Outdated

please, consider reduction of nesting

please, consider reduction of nesting

Corrected

Corrected
state->bom_len = ff_skip_bom(fp);
state->n_entries = 0;
state->total_lines = 0;
// Clear ID caches for full rebuild
g_hash_table_remove_all(state->archive_ids);
g_hash_table_remove_all(state->stanza_senders);
_ff_scan_lines(state, fp);
fclose(fp); fclose(fp);
@@ -247,37 +254,8 @@ _ff_state_extend(ff_contact_state_t* state)
} }
} }
// Use a local counter for new messages so index step alignment // Use _ff_scan_lines for the shared read/index/cache loop.
// doesn't depend on the total from the initial build. _ff_scan_lines(state, fp);
size_t new_count = 0;
while (1) {
off_t pos = ftell(fp);
gboolean truncated = FALSE;
char* buf = ff_readline(fp, &truncated);
if (!buf)
break;
if (truncated) {
free(buf);
break;
}
if (buf[0] == '\0' || buf[0] == '#') {
free(buf);
continue;
}
state->total_lines++;
// Extract IDs for O(1) dedup/LMC cache
_ff_cache_line_ids(state, buf);
if (new_count % FF_INDEX_STEP == 0) {
_ff_maybe_index_line(state, buf, pos);
}
new_count++;
free(buf);
}
fclose(fp); fclose(fp);