1157 lines
37 KiB
C
1157 lines
37 KiB
C
/*
|
|
* db.c — SQLite storage for sovereign_browser
|
|
*
|
|
* Stores Nostr events and misc data in ~/.sovereign_browser/browser.db.
|
|
* Uses SQLITE_OPEN_FULLMUTEX for thread safety (the relay fetch runs in
|
|
* a background thread).
|
|
*/
|
|
|
|
#include "db.h"
|
|
|
|
#include <sqlite3.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
|
|
/* ── Global state ──────────────────────────────────────────────────── */
|
|
|
|
static sqlite3 *g_db = NULL;
|
|
|
|
/* Static buffer for db_kv_get() — valid until the next call. */
|
|
static char g_kv_buf[4096];
|
|
|
|
/* ── Path helpers ──────────────────────────────────────────────────── */
|
|
|
|
/* ~/.sovereign_browser/ (created if missing). Returns 0 on success. */
|
|
static int sb_home_dir(char *out, size_t out_sz) {
|
|
const char *home = getenv("HOME");
|
|
if (home == NULL || home[0] == '\0') {
|
|
return -1;
|
|
}
|
|
int n = snprintf(out, out_sz, "%s/.sovereign_browser", home);
|
|
if (n < 0 || (size_t)n >= out_sz) {
|
|
return -1;
|
|
}
|
|
if (mkdir(out, 0700) != 0 && errno != EEXIST) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ~/.sovereign_browser/browser.db (legacy default per-user path). */
|
|
static int db_path(char *out, size_t out_sz) {
|
|
char dir[512];
|
|
if (sb_home_dir(dir, sizeof(dir)) != 0) {
|
|
return -1;
|
|
}
|
|
int n = snprintf(out, out_sz, "%s/browser.db", dir);
|
|
if (n < 0 || (size_t)n >= out_sz) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ~/.sovereign_browser/global.db (global settings + shortcuts). */
|
|
static int db_global_path(char *out, size_t out_sz) {
|
|
char dir[512];
|
|
if (sb_home_dir(dir, sizeof(dir)) != 0) {
|
|
return -1;
|
|
}
|
|
int n = snprintf(out, out_sz, "%s/global.db", dir);
|
|
if (n < 0 || (size_t)n >= out_sz) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ── Schema ────────────────────────────────────────────────────────── */
|
|
|
|
static const char *SCHEMA_SQL =
|
|
"CREATE TABLE IF NOT EXISTS events ("
|
|
" id TEXT PRIMARY KEY,"
|
|
" pubkey TEXT NOT NULL,"
|
|
" kind INTEGER NOT NULL,"
|
|
" created_at INTEGER NOT NULL,"
|
|
" content TEXT,"
|
|
" sig TEXT,"
|
|
" raw_json TEXT NOT NULL,"
|
|
" fetched_at INTEGER NOT NULL"
|
|
");"
|
|
"CREATE TABLE IF NOT EXISTS event_tags ("
|
|
" event_id TEXT NOT NULL,"
|
|
" tag_name TEXT NOT NULL,"
|
|
" tag_value TEXT,"
|
|
" position INTEGER NOT NULL,"
|
|
" FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE"
|
|
");"
|
|
"CREATE TABLE IF NOT EXISTS key_value ("
|
|
" key TEXT PRIMARY KEY,"
|
|
" value TEXT"
|
|
");"
|
|
"CREATE TABLE IF NOT EXISTS history ("
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
" url TEXT NOT NULL UNIQUE,"
|
|
" title TEXT,"
|
|
" visited_at INTEGER NOT NULL,"
|
|
" visit_count INTEGER DEFAULT 1"
|
|
");"
|
|
"CREATE INDEX IF NOT EXISTS idx_history_visited_at "
|
|
" ON history(visited_at DESC);"
|
|
"CREATE INDEX IF NOT EXISTS idx_history_url "
|
|
" ON history(url);"
|
|
"CREATE INDEX IF NOT EXISTS idx_history_title "
|
|
" ON history(title);"
|
|
"CREATE INDEX IF NOT EXISTS idx_history_visit_count "
|
|
" ON history(visit_count DESC);"
|
|
"CREATE TABLE IF NOT EXISTS session ("
|
|
" tab_index INTEGER PRIMARY KEY,"
|
|
" url TEXT NOT NULL,"
|
|
" title TEXT"
|
|
");"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind "
|
|
" ON events(pubkey, kind, created_at DESC);"
|
|
"CREATE INDEX IF NOT EXISTS idx_event_tags_name_value "
|
|
" ON event_tags(tag_name, tag_value);"
|
|
"CREATE INDEX IF NOT EXISTS idx_event_tags_event_id "
|
|
" ON event_tags(event_id);"
|
|
"CREATE TABLE IF NOT EXISTS agent_sessions ("
|
|
" id TEXT PRIMARY KEY,"
|
|
" title TEXT,"
|
|
" created_at INTEGER,"
|
|
" updated_at INTEGER"
|
|
");"
|
|
"CREATE TABLE IF NOT EXISTS agent_messages ("
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
" session_id TEXT,"
|
|
" role TEXT,"
|
|
" content TEXT,"
|
|
" tool_calls TEXT,"
|
|
" tool_call_id TEXT,"
|
|
" created_at INTEGER,"
|
|
" FOREIGN KEY (session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE"
|
|
");"
|
|
"CREATE INDEX IF NOT EXISTS idx_agent_messages_session "
|
|
" ON agent_messages(session_id, created_at);";
|
|
|
|
/* ── Init / Close ──────────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* Open a SQLite database at the given path with the standard schema.
|
|
* Closes any currently-open database first. Returns 0 on success.
|
|
*/
|
|
int db_init_with_path(const char *path) {
|
|
if (path == NULL || path[0] == '\0') {
|
|
g_printerr("[db] db_init_with_path: NULL path\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Close any currently-open database so we can switch databases. */
|
|
if (g_db) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
}
|
|
|
|
int rc = sqlite3_open_v2(path, &g_db,
|
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
|
|
SQLITE_OPEN_FULLMUTEX,
|
|
NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Failed to open database '%s': %s\n",
|
|
path, g_db ? sqlite3_errmsg(g_db) : "(null)");
|
|
if (g_db) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Set busy timeout to 5 seconds (in case of contention). */
|
|
sqlite3_busy_timeout(g_db, 5000);
|
|
|
|
/* Enable foreign keys (for ON DELETE CASCADE on event_tags). */
|
|
char *err = NULL;
|
|
rc = sqlite3_exec(g_db, "PRAGMA foreign_keys = ON;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Failed to enable foreign keys: %s\n",
|
|
err ? err : "(unknown)");
|
|
if (err) sqlite3_free(err);
|
|
}
|
|
|
|
/* Create schema (all tables use IF NOT EXISTS, so this is safe for
|
|
* both global.db and per-user browser.db). */
|
|
rc = sqlite3_exec(g_db, SCHEMA_SQL, NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Failed to create schema: %s\n",
|
|
err ? err : "(unknown)");
|
|
if (err) sqlite3_free(err);
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
g_print("[db] Database initialized at %s\n", path);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Open the global database at ~/.sovereign_browser/global.db.
|
|
* Used at startup for global settings + shortcuts, before login.
|
|
*/
|
|
int db_init_global(void) {
|
|
char path[512];
|
|
if (db_global_path(path, sizeof(path)) != 0) {
|
|
g_printerr("[db] Failed to get global database path\n");
|
|
return -1;
|
|
}
|
|
return db_init_with_path(path);
|
|
}
|
|
|
|
/*
|
|
* Open the default per-user database at ~/.sovereign_browser/browser.db.
|
|
* Kept for compatibility; the normal flow now uses db_init_with_path()
|
|
* with a profile-specific path after login.
|
|
*/
|
|
int db_init(void) {
|
|
char path[512];
|
|
if (db_path(path, sizeof(path)) != 0) {
|
|
g_printerr("[db] Failed to get database path\n");
|
|
return -1;
|
|
}
|
|
return db_init_with_path(path);
|
|
}
|
|
|
|
void db_close(void) {
|
|
if (g_db) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
g_print("[db] Database closed\n");
|
|
}
|
|
}
|
|
|
|
/* ── Events ────────────────────────────────────────────────────────── */
|
|
|
|
/* Helper: get a string field from a cJSON event, or "" if missing. */
|
|
static const char *event_str(const cJSON *event, const char *field) {
|
|
cJSON *item = cJSON_GetObjectItemCaseSensitive(event, field);
|
|
if (cJSON_IsString(item)) return item->valuestring;
|
|
return "";
|
|
}
|
|
|
|
/* Helper: get an integer field from a cJSON event, or 0 if missing. */
|
|
static long event_int(const cJSON *event, const char *field) {
|
|
cJSON *item = cJSON_GetObjectItemCaseSensitive(event, field);
|
|
if (cJSON_IsNumber(item)) return (long)item->valuedouble;
|
|
return 0;
|
|
}
|
|
|
|
int db_store_event(const cJSON *event) {
|
|
if (g_db == NULL || event == NULL) return -1;
|
|
|
|
const char *id = event_str(event, "id");
|
|
const char *pubkey = event_str(event, "pubkey");
|
|
long kind = event_int(event, "kind");
|
|
long created = event_int(event, "created_at");
|
|
const char *content = event_str(event, "content");
|
|
const char *sig = event_str(event, "sig");
|
|
|
|
if (id[0] == '\0' || pubkey[0] == '\0') {
|
|
g_printerr("[db] Event missing id or pubkey\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Serialize the full event to JSON for round-tripping. */
|
|
char *raw_json = cJSON_PrintUnformatted(event);
|
|
if (raw_json == NULL) return -1;
|
|
|
|
/* Begin transaction. */
|
|
char *err = NULL;
|
|
int rc = sqlite3_exec(g_db, "BEGIN TRANSACTION;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] BEGIN failed: %s\n", err ? err : "(unknown)");
|
|
if (err) sqlite3_free(err);
|
|
free(raw_json);
|
|
return -1;
|
|
}
|
|
|
|
/* Upsert the event (INSERT OR REPLACE). */
|
|
sqlite3_stmt *stmt = NULL;
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT OR REPLACE INTO events "
|
|
"(id, pubkey, kind, created_at, content, sig, raw_json, fetched_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%s','now'));",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Prepare events insert failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
free(raw_json);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 3, kind);
|
|
sqlite3_bind_int64(stmt, 4, created);
|
|
sqlite3_bind_text(stmt, 5, content, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 6, sig, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 7, raw_json, -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] Events insert failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
free(raw_json);
|
|
return -1;
|
|
}
|
|
|
|
/* Delete old tags for this event (if replacing) and insert new ones. */
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM event_tags WHERE event_id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
/* Insert tags. */
|
|
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (cJSON_IsArray(tags)) {
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO event_tags (event_id, tag_name, tag_value, position) "
|
|
"VALUES (?, ?, ?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
int pos = 0;
|
|
cJSON *tag;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!cJSON_IsArray(tag)) continue;
|
|
cJSON *name_item = cJSON_GetArrayItem(tag, 0);
|
|
cJSON *value_item = cJSON_GetArrayItem(tag, 1);
|
|
const char *tag_name = (name_item && cJSON_IsString(name_item))
|
|
? name_item->valuestring : "";
|
|
const char *tag_value = (value_item && cJSON_IsString(value_item))
|
|
? value_item->valuestring : "";
|
|
|
|
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, tag_name, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, tag_value, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 4, pos);
|
|
sqlite3_step(stmt);
|
|
sqlite3_reset(stmt);
|
|
pos++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
|
|
/* Commit. */
|
|
rc = sqlite3_exec(g_db, "COMMIT;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] COMMIT failed: %s\n", err ? err : "(unknown)");
|
|
if (err) sqlite3_free(err);
|
|
free(raw_json);
|
|
return -1;
|
|
}
|
|
|
|
free(raw_json);
|
|
return 0;
|
|
}
|
|
|
|
cJSON *db_get_latest_event(const char *pubkey_hex, int kind) {
|
|
if (g_db == NULL || pubkey_hex == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT raw_json FROM events "
|
|
"WHERE pubkey = ? AND kind = ? "
|
|
"ORDER BY created_at DESC LIMIT 1;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Prepare latest_event failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, pubkey_hex, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 2, kind);
|
|
|
|
cJSON *result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *json_str = (const char *)sqlite3_column_text(stmt, 0);
|
|
if (json_str) {
|
|
result = cJSON_Parse(json_str);
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
cJSON *db_get_events(const char *pubkey_hex, int kind, int limit) {
|
|
if (g_db == NULL || pubkey_hex == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
/* If limit is 0, use a large default. */
|
|
int lim = (limit > 0) ? limit : 10000;
|
|
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT raw_json FROM events "
|
|
"WHERE pubkey = ? AND kind = ? "
|
|
"ORDER BY created_at DESC LIMIT ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Prepare get_events failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, pubkey_hex, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 2, kind);
|
|
sqlite3_bind_int(stmt, 3, lim);
|
|
|
|
cJSON *array = cJSON_CreateArray();
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *json_str = (const char *)sqlite3_column_text(stmt, 0);
|
|
if (json_str) {
|
|
cJSON *event = cJSON_Parse(json_str);
|
|
if (event) {
|
|
cJSON_AddItemToArray(array, event);
|
|
}
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return array;
|
|
}
|
|
|
|
int db_count_events(const char *pubkey_hex, int kind) {
|
|
if (g_db == NULL || pubkey_hex == NULL) return -1;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT COUNT(*) FROM events WHERE pubkey = ? AND kind = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, pubkey_hex, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 2, kind);
|
|
|
|
int count = -1;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return count;
|
|
}
|
|
|
|
cJSON *db_get_events_by_kind(int kind, int limit) {
|
|
if (g_db == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int lim = (limit > 0) ? limit : 10000;
|
|
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT raw_json FROM events "
|
|
"WHERE kind = ? "
|
|
"ORDER BY created_at DESC LIMIT ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] Prepare get_events_by_kind failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_int(stmt, 1, kind);
|
|
sqlite3_bind_int(stmt, 2, lim);
|
|
|
|
cJSON *array = cJSON_CreateArray();
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *json_str = (const char *)sqlite3_column_text(stmt, 0);
|
|
if (json_str) {
|
|
cJSON *event = cJSON_Parse(json_str);
|
|
if (event) {
|
|
cJSON_AddItemToArray(array, event);
|
|
}
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return array;
|
|
}
|
|
|
|
int db_delete_event(const char *event_id) {
|
|
if (g_db == NULL || event_id == NULL) return -1;
|
|
|
|
char *err = NULL;
|
|
int rc = sqlite3_exec(g_db, "BEGIN TRANSACTION;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
if (err) sqlite3_free(err);
|
|
return -1;
|
|
}
|
|
|
|
/* Delete tags first. */
|
|
sqlite3_stmt *stmt = NULL;
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM event_tags WHERE event_id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
/* Delete the event row. */
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM events WHERE id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] delete_event prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
return -1;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] delete_event step failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
return -1;
|
|
}
|
|
|
|
rc = sqlite3_exec(g_db, "COMMIT;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
if (err) sqlite3_free(err);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ── Key-Value store ───────────────────────────────────────────────── */
|
|
|
|
int db_kv_set(const char *key, const char *value) {
|
|
if (g_db == NULL || key == NULL || value == NULL) return -1;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT OR REPLACE INTO key_value (key, value) VALUES (?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
/*
|
|
* Write a key-value pair to a specific database file, opening a separate
|
|
* short-lived connection. This is used to save global settings to
|
|
* global.db while the per-user browser.db is the main open database.
|
|
* Creates the key_value table if it doesn't exist (safe for a fresh
|
|
* global.db). Returns 0 on success, -1 on error.
|
|
*/
|
|
int db_kv_set_to_file(const char *path, const char *key, const char *value) {
|
|
if (path == NULL || key == NULL || value == NULL) return -1;
|
|
|
|
sqlite3 *db = NULL;
|
|
int rc = sqlite3_open_v2(path, &db,
|
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
|
|
SQLITE_OPEN_FULLMUTEX,
|
|
NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] db_kv_set_to_file: open '%s' failed: %s\n",
|
|
path, db ? sqlite3_errmsg(db) : "(null)");
|
|
if (db) sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_busy_timeout(db, 5000);
|
|
|
|
/* Ensure the key_value table exists (harmless if already present). */
|
|
char *err = NULL;
|
|
rc = sqlite3_exec(db,
|
|
"CREATE TABLE IF NOT EXISTS key_value ("
|
|
" key TEXT PRIMARY KEY,"
|
|
" value TEXT"
|
|
");", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] db_kv_set_to_file: create table failed: %s\n",
|
|
err ? err : "(unknown)");
|
|
if (err) sqlite3_free(err);
|
|
sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
rc = sqlite3_prepare_v2(db,
|
|
"INSERT OR REPLACE INTO key_value (key, value) VALUES (?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] db_kv_set_to_file: prepare failed: %s\n",
|
|
sqlite3_errmsg(db));
|
|
sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
const char *db_kv_get(const char *key) {
|
|
if (g_db == NULL || key == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT value FROM key_value WHERE key = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return NULL;
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
|
|
|
const char *result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
|
if (val) {
|
|
snprintf(g_kv_buf, sizeof(g_kv_buf), "%s", val);
|
|
result = g_kv_buf;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
char *db_kv_get_copy(const char *key) {
|
|
const char *val = db_kv_get(key);
|
|
if (val == NULL) return NULL;
|
|
return g_strdup(val);
|
|
}
|
|
|
|
/* ── History ───────────────────────────────────────────────────────── */
|
|
|
|
int db_history_add(const char *url, const char *title) {
|
|
if (g_db == NULL || url == NULL || url[0] == '\0') return -1;
|
|
|
|
/* UPSERT: insert or update visited_at + visit_count. */
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO history (url, title, visited_at, visit_count) "
|
|
"VALUES (?, ?, strftime('%s','now'), 1) "
|
|
"ON CONFLICT(url) DO UPDATE SET "
|
|
" title = excluded.title, "
|
|
" visited_at = strftime('%s','now'), "
|
|
" visit_count = visit_count + 1;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_text(stmt, 1, url, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, title ? title : "", -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return (rc == SQLITE_DONE) ? 0 : -1;
|
|
}
|
|
|
|
int db_history_get(char ***urls_out, char ***titles_out,
|
|
int *count_out, int limit) {
|
|
if (g_db == NULL || urls_out == NULL || count_out == NULL) return -1;
|
|
if (limit <= 0) limit = 50;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT url, title FROM history "
|
|
"ORDER BY visited_at DESC LIMIT ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
sqlite3_bind_int(stmt, 1, limit);
|
|
|
|
/* First pass: count rows. */
|
|
int count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) count++;
|
|
sqlite3_reset(stmt);
|
|
|
|
/* Allocate arrays. */
|
|
*urls_out = g_new0(char *, count);
|
|
if (titles_out) *titles_out = g_new0(char *, count);
|
|
if (count_out) *count_out = 0;
|
|
|
|
/* Second pass: fill. */
|
|
int i = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW && i < count) {
|
|
const char *url = (const char *)sqlite3_column_text(stmt, 0);
|
|
const char *title = (const char *)sqlite3_column_text(stmt, 1);
|
|
(*urls_out)[i] = g_strdup(url ? url : "");
|
|
if (titles_out) {
|
|
(*titles_out)[i] = g_strdup(title ? title : "");
|
|
}
|
|
i++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (count_out) *count_out = i;
|
|
return i;
|
|
}
|
|
|
|
int db_history_clear(void) {
|
|
if (g_db == NULL) return -1;
|
|
char *err = NULL;
|
|
int rc = sqlite3_exec(g_db, "DELETE FROM history;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
if (err) sqlite3_free(err);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ── History search ────────────────────────────────────────────────── */
|
|
|
|
int db_history_search(const char *query,
|
|
char ***urls_out, char ***titles_out,
|
|
int *count_out, int limit) {
|
|
if (g_db == NULL || query == NULL || urls_out == NULL || count_out == NULL)
|
|
return -1;
|
|
if (limit <= 0) limit = 10;
|
|
|
|
/* Build a LIKE pattern: %query% (case-insensitive via COLLATE NOCASE
|
|
* is not needed — SQLite LIKE is case-insensitive for ASCII by default). */
|
|
char *pattern = g_strdup_printf("%%%s%%", query);
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT url, title FROM history "
|
|
"WHERE url LIKE ? OR title LIKE ? "
|
|
"ORDER BY visit_count DESC, visited_at DESC LIMIT ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_free(pattern);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, pattern, -1, g_free);
|
|
sqlite3_bind_text(stmt, 2, pattern, -1, NULL); /* same pattern, freed once */
|
|
sqlite3_bind_int(stmt, 3, limit);
|
|
|
|
/* First pass: count rows. */
|
|
int count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) count++;
|
|
sqlite3_reset(stmt);
|
|
|
|
/* Re-bind (sqlite3_reset keeps bindings, but reset clears them in some
|
|
* versions — re-bind to be safe). */
|
|
char *pattern2 = g_strdup_printf("%%%s%%", query);
|
|
sqlite3_bind_text(stmt, 1, pattern2, -1, g_free);
|
|
sqlite3_bind_text(stmt, 2, pattern2, -1, NULL);
|
|
sqlite3_bind_int(stmt, 3, limit);
|
|
|
|
/* Allocate arrays. */
|
|
*urls_out = g_new0(char *, count);
|
|
if (titles_out) *titles_out = g_new0(char *, count);
|
|
if (count_out) *count_out = 0;
|
|
|
|
/* Second pass: fill. */
|
|
int i = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW && i < count) {
|
|
const char *url = (const char *)sqlite3_column_text(stmt, 0);
|
|
const char *title = (const char *)sqlite3_column_text(stmt, 1);
|
|
(*urls_out)[i] = g_strdup(url ? url : "");
|
|
if (titles_out) {
|
|
(*titles_out)[i] = g_strdup(title ? title : "");
|
|
}
|
|
i++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (count_out) *count_out = i;
|
|
return i;
|
|
}
|
|
|
|
/* ── Session ───────────────────────────────────────────────────────── */
|
|
|
|
int db_session_save(const char **urls, const char **titles, int count) {
|
|
if (g_db == NULL || urls == NULL) return -1;
|
|
|
|
/* Clear the session table first. */
|
|
char *err = NULL;
|
|
int rc = sqlite3_exec(g_db, "DELETE FROM session;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
if (err) sqlite3_free(err);
|
|
return -1;
|
|
}
|
|
|
|
/* Insert each tab. */
|
|
sqlite3_stmt *stmt = NULL;
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO session (tab_index, url, title) VALUES (?, ?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (urls[i] == NULL) continue;
|
|
sqlite3_bind_int(stmt, 1, i);
|
|
sqlite3_bind_text(stmt, 2, urls[i], -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3,
|
|
(titles && titles[i]) ? titles[i] : "",
|
|
-1, SQLITE_TRANSIENT);
|
|
sqlite3_step(stmt);
|
|
sqlite3_reset(stmt);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return 0;
|
|
}
|
|
|
|
int db_session_load(char ***urls_out, char ***titles_out, int *count_out) {
|
|
if (g_db == NULL || urls_out == NULL || count_out == NULL) return -1;
|
|
|
|
/* Count rows. */
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT url, title FROM session ORDER BY tab_index;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) return -1;
|
|
|
|
int count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) count++;
|
|
sqlite3_reset(stmt);
|
|
|
|
if (count == 0) {
|
|
sqlite3_finalize(stmt);
|
|
*count_out = 0;
|
|
return 0;
|
|
}
|
|
|
|
*urls_out = g_new0(char *, count);
|
|
if (titles_out) *titles_out = g_new0(char *, count);
|
|
|
|
int i = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW && i < count) {
|
|
const char *url = (const char *)sqlite3_column_text(stmt, 0);
|
|
const char *title = (const char *)sqlite3_column_text(stmt, 1);
|
|
(*urls_out)[i] = g_strdup(url ? url : "");
|
|
if (titles_out) {
|
|
(*titles_out)[i] = g_strdup(title ? title : "");
|
|
}
|
|
i++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
*count_out = i;
|
|
return i;
|
|
}
|
|
|
|
int db_session_clear(void) {
|
|
if (g_db == NULL) return -1;
|
|
char *err = NULL;
|
|
int rc = sqlite3_exec(g_db, "DELETE FROM session;", NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
if (err) sqlite3_free(err);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ── Agent chat sessions ───────────────────────────────────────────── */
|
|
|
|
char *db_agent_session_create(const char *title) {
|
|
if (g_db == NULL) return NULL;
|
|
|
|
char *id = g_uuid_string_random();
|
|
if (id == NULL) {
|
|
g_printerr("[db] agent_session_create: g_uuid_string_random failed\n");
|
|
return NULL;
|
|
}
|
|
|
|
long now = (long)time(NULL);
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO agent_sessions (id, title, created_at, updated_at) "
|
|
"VALUES (?, ?, ?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_create: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
g_free(id);
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, title ? title : "", -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 3, now);
|
|
sqlite3_bind_int64(stmt, 4, now);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] agent_session_create: insert failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
g_free(id);
|
|
return NULL;
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
char *db_agent_session_get_latest(void) {
|
|
if (g_db == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT id FROM agent_sessions ORDER BY updated_at DESC LIMIT 1;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_get_latest: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return NULL;
|
|
}
|
|
|
|
char *result = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *id = (const char *)sqlite3_column_text(stmt, 0);
|
|
if (id) {
|
|
result = g_strdup(id);
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
int db_agent_session_update(const char *session_id, const char *title) {
|
|
if (g_db == NULL || session_id == NULL) return -1;
|
|
|
|
long now = (long)time(NULL);
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"UPDATE agent_sessions SET title = ?, updated_at = ? "
|
|
"WHERE id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_update: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, title ? title : "", -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(stmt, 2, now);
|
|
sqlite3_bind_text(stmt, 3, session_id, -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] agent_session_update: update failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int db_agent_session_list(char ***ids_out, char ***titles_out,
|
|
int *count_out) {
|
|
if (g_db == NULL || ids_out == NULL || titles_out == NULL ||
|
|
count_out == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT id, title FROM agent_sessions "
|
|
"ORDER BY updated_at DESC;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_list: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
/* First pass: count rows. */
|
|
int count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) count++;
|
|
sqlite3_reset(stmt);
|
|
|
|
/* Allocate arrays. */
|
|
*ids_out = g_new0(char *, count);
|
|
*titles_out = g_new0(char *, count);
|
|
*count_out = 0;
|
|
|
|
/* Second pass: fill. */
|
|
int i = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW && i < count) {
|
|
const char *id = (const char *)sqlite3_column_text(stmt, 0);
|
|
const char *title = (const char *)sqlite3_column_text(stmt, 1);
|
|
(*ids_out)[i] = g_strdup(id ? id : "");
|
|
(*titles_out)[i] = g_strdup(title ? title : "");
|
|
i++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
*count_out = i;
|
|
return i;
|
|
}
|
|
|
|
int db_agent_message_add(const char *session_id, const char *role,
|
|
const char *content, const char *tool_calls,
|
|
const char *tool_call_id) {
|
|
if (g_db == NULL || session_id == NULL || role == NULL) return -1;
|
|
|
|
long now = (long)time(NULL);
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO agent_messages "
|
|
"(session_id, role, content, tool_calls, tool_call_id, created_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?);",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_message_add: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, role, -1, SQLITE_TRANSIENT);
|
|
if (content) {
|
|
sqlite3_bind_text(stmt, 3, content, -1, SQLITE_TRANSIENT);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 3);
|
|
}
|
|
if (tool_calls) {
|
|
sqlite3_bind_text(stmt, 4, tool_calls, -1, SQLITE_TRANSIENT);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 4);
|
|
}
|
|
if (tool_call_id) {
|
|
sqlite3_bind_text(stmt, 5, tool_call_id, -1, SQLITE_TRANSIENT);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 5);
|
|
}
|
|
sqlite3_bind_int64(stmt, 6, now);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] agent_message_add: insert failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
return (int)sqlite3_last_insert_rowid(g_db);
|
|
}
|
|
|
|
cJSON *db_agent_message_list(const char *session_id) {
|
|
if (g_db == NULL || session_id == NULL) return NULL;
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"SELECT id, role, content, tool_calls, tool_call_id "
|
|
"FROM agent_messages WHERE session_id = ? "
|
|
"ORDER BY created_at ASC, id ASC;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_message_list: prepare failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return NULL;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
|
|
cJSON *array = cJSON_CreateArray();
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
cJSON *msg = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(msg, "id",
|
|
(double)sqlite3_column_int64(stmt, 0));
|
|
|
|
const char *role = (const char *)sqlite3_column_text(stmt, 1);
|
|
cJSON_AddStringToObject(msg, "role", role ? role : "");
|
|
|
|
/* content may be NULL. */
|
|
if (sqlite3_column_type(stmt, 2) == SQLITE_NULL) {
|
|
cJSON_AddNullToObject(msg, "content");
|
|
} else {
|
|
const char *content = (const char *)sqlite3_column_text(stmt, 2);
|
|
cJSON_AddStringToObject(msg, "content", content ? content : "");
|
|
}
|
|
|
|
/* tool_calls may be NULL. */
|
|
if (sqlite3_column_type(stmt, 3) == SQLITE_NULL) {
|
|
cJSON_AddNullToObject(msg, "tool_calls");
|
|
} else {
|
|
const char *tc = (const char *)sqlite3_column_text(stmt, 3);
|
|
cJSON_AddStringToObject(msg, "tool_calls", tc ? tc : "");
|
|
}
|
|
|
|
/* tool_call_id may be NULL. */
|
|
if (sqlite3_column_type(stmt, 4) == SQLITE_NULL) {
|
|
cJSON_AddNullToObject(msg, "tool_call_id");
|
|
} else {
|
|
const char *tcid = (const char *)sqlite3_column_text(stmt, 4);
|
|
cJSON_AddStringToObject(msg, "tool_call_id", tcid ? tcid : "");
|
|
}
|
|
|
|
cJSON_AddItemToArray(array, msg);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return array;
|
|
}
|
|
|
|
int db_agent_session_delete(const char *session_id) {
|
|
if (g_db == NULL || session_id == NULL) return -1;
|
|
|
|
/* Delete messages first (in case foreign keys are not enforced). */
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM agent_messages WHERE session_id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_delete: prepare messages failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] agent_session_delete: delete messages failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
|
|
/* Delete the session row. */
|
|
rc = sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM agent_sessions WHERE id = ?;",
|
|
-1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
g_printerr("[db] agent_session_delete: prepare session failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
g_printerr("[db] agent_session_delete: delete session failed: %s\n",
|
|
sqlite3_errmsg(g_db));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|