2026-01-28 19:34:39 +01:00
|
|
|
// retoor <retoor@molodetz.nl>
|
|
|
|
|
#include "db.h"
|
|
|
|
|
#include "r_config.h"
|
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
struct db_t {
|
|
|
|
|
sqlite3 *conn;
|
|
|
|
|
char *path;
|
|
|
|
|
};
|
|
|
|
|
static char *expand_home_directory(const char *path) {
|
|
|
|
|
if (!path) return NULL;
|
|
|
|
|
if (path[0] != '~') return strdup(path);
|
|
|
|
|
const char *home_dir = getenv("HOME");
|
|
|
|
|
if (!home_dir) home_dir = getenv("USERPROFILE");
|
|
|
|
|
if (!home_dir) return strdup(path);
|
|
|
|
|
size_t home_len = strlen(home_dir);
|
|
|
|
|
size_t path_len = strlen(path);
|
2026-04-03 10:42:43 +02:00
|
|
|
char *expanded = malloc(home_len + path_len + 1);
|
2026-01-28 19:34:39 +01:00
|
|
|
if (!expanded) return NULL;
|
2026-04-03 10:42:43 +02:00
|
|
|
memcpy(expanded, home_dir, home_len);
|
|
|
|
|
memcpy(expanded + home_len, path + 1, path_len);
|
2026-01-28 19:34:39 +01:00
|
|
|
return expanded;
|
|
|
|
|
}
|
|
|
|
|
db_handle db_open(const char *path) {
|
|
|
|
|
struct db_t *db = calloc(1, sizeof(struct db_t));
|
|
|
|
|
if (!db) return NULL;
|
|
|
|
|
if (!path) {
|
|
|
|
|
r_config_handle cfg = r_config_get_instance();
|
|
|
|
|
path = r_config_get_db_path(cfg);
|
|
|
|
|
}
|
|
|
|
|
db->path = expand_home_directory(path);
|
|
|
|
|
if (!db->path) {
|
|
|
|
|
free(db);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
if (sqlite3_open(db->path, &db->conn) != SQLITE_OK) {
|
|
|
|
|
free(db->path);
|
|
|
|
|
free(db);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
db_init(db);
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
void db_close(db_handle db) {
|
|
|
|
|
if (!db) return;
|
|
|
|
|
if (db->conn) sqlite3_close(db->conn);
|
|
|
|
|
free(db->path);
|
|
|
|
|
free(db);
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_init(db_handle db) {
|
|
|
|
|
if (!db || !db->conn) return R_ERROR_INVALID_ARG;
|
|
|
|
|
const char *sql =
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS kv ("
|
|
|
|
|
" key TEXT PRIMARY KEY,"
|
|
|
|
|
" value TEXT NOT NULL,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS file_versions ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" path TEXT NOT NULL,"
|
|
|
|
|
" content TEXT NOT NULL,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS conversations ("
|
|
|
|
|
" session_key TEXT PRIMARY KEY,"
|
|
|
|
|
" data TEXT NOT NULL,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
chore: remove entire agent, auth, browse, chat, db_utils, and http_curl source trees
This commit deletes the complete implementation of the agent loop, authentication module, web browsing utilities, chat prompt builder, database helper, and HTTP client. All corresponding header files and test harnesses are also removed. The diff shows the deletion of 6 source files and 6 header files totaling over 1000 lines of code, including the agent state machine, API key resolution logic, curl-based HTTP functions, SQLite database initialization and CRUD operations, and the JSON chat prompt construction.
2026-01-29 02:21:11 +01:00
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS blackboard ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" sender TEXT NOT NULL,"
|
|
|
|
|
" recipient TEXT,"
|
|
|
|
|
" topic TEXT NOT NULL,"
|
|
|
|
|
" message TEXT NOT NULL,"
|
|
|
|
|
" status TEXT DEFAULT 'pending',"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
2026-02-10 04:29:48 +01:00
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS gtr ("
|
|
|
|
|
" task_hash TEXT PRIMARY KEY,"
|
|
|
|
|
" task_description TEXT NOT NULL,"
|
|
|
|
|
" status TEXT NOT NULL,"
|
|
|
|
|
" owner_agent TEXT NOT NULL,"
|
|
|
|
|
" result_summary TEXT,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS audit_log ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" issuer_agent TEXT NOT NULL,"
|
|
|
|
|
" recipient_agent TEXT NOT NULL,"
|
|
|
|
|
" order_description TEXT NOT NULL,"
|
|
|
|
|
" logic_justification TEXT NOT NULL,"
|
|
|
|
|
" status TEXT NOT NULL"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS agents ("
|
|
|
|
|
" agent_id TEXT PRIMARY KEY,"
|
|
|
|
|
" role TEXT NOT NULL,"
|
|
|
|
|
" manager_id TEXT,"
|
|
|
|
|
" department TEXT,"
|
|
|
|
|
" budget_limit_tokens INTEGER DEFAULT 1000000,"
|
|
|
|
|
" used_tokens INTEGER DEFAULT 0,"
|
|
|
|
|
" last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" status TEXT DEFAULT 'active'"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS research_tasks ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" url_hash TEXT UNIQUE,"
|
|
|
|
|
" url TEXT NOT NULL,"
|
|
|
|
|
" status TEXT NOT NULL,"
|
|
|
|
|
" summary TEXT,"
|
|
|
|
|
" batch_id TEXT,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
2026-02-14 08:07:05 +01:00
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS snapshots ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" session_id TEXT NOT NULL,"
|
|
|
|
|
" description TEXT,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS snapshot_files ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" snapshot_id INTEGER NOT NULL,"
|
|
|
|
|
" path TEXT NOT NULL,"
|
|
|
|
|
" content TEXT NOT NULL,"
|
|
|
|
|
" FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshot_files_unique "
|
|
|
|
|
"ON snapshot_files(snapshot_id, path);";
|
2026-01-28 19:34:39 +01:00
|
|
|
char *err_msg = NULL;
|
|
|
|
|
if (sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg) != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_kv_set(db_handle db, const char *key, const char *value) {
|
|
|
|
|
if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT OR REPLACE INTO kv (key, value, updated_at) VALUES (%Q, %Q, CURRENT_TIMESTAMP)",
|
|
|
|
|
key, value);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_kv_get(db_handle db, const char *key, char **value) {
|
|
|
|
|
if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG;
|
|
|
|
|
*value = NULL;
|
|
|
|
|
const char *sql = "SELECT value FROM kv WHERE key = ?";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
|
|
|
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
|
|
|
|
*value = val ? strdup(val) : NULL;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_execute(db_handle db, const char *sql, struct json_object **result) {
|
|
|
|
|
if (!db || !db->conn || !sql || !result) return R_ERROR_INVALID_ARG;
|
|
|
|
|
*result = NULL;
|
|
|
|
|
const char *select_check = sql;
|
|
|
|
|
while (*select_check == ' ') select_check++;
|
|
|
|
|
if (strncasecmp(select_check, "SELECT", 6) != 0) {
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
struct json_object *error_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(error_obj, "error",
|
|
|
|
|
json_object_new_string(err_msg ? err_msg : "Query failed"));
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
*result = error_obj;
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
struct json_object *success_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(success_obj, "success", json_object_new_boolean(1));
|
|
|
|
|
*result = success_obj;
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
struct json_object *error_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(error_obj, "error",
|
|
|
|
|
json_object_new_string(sqlite3_errmsg(db->conn)));
|
|
|
|
|
*result = error_obj;
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
struct json_object *array = json_object_new_array();
|
|
|
|
|
int col_count = sqlite3_column_count(stmt);
|
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
struct json_object *row = json_object_new_object();
|
|
|
|
|
for (int i = 0; i < col_count; i++) {
|
|
|
|
|
const char *col_name = sqlite3_column_name(stmt, i);
|
|
|
|
|
int col_type = sqlite3_column_type(stmt, i);
|
|
|
|
|
switch (col_type) {
|
|
|
|
|
case SQLITE_INTEGER:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_int64(sqlite3_column_int64(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_FLOAT:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_double(sqlite3_column_double(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_TEXT:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_string((const char *)sqlite3_column_text(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_NULL:
|
|
|
|
|
json_object_object_add(row, col_name, NULL);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_string((const char *)sqlite3_column_text(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
json_object_array_add(array, row);
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
*result = array;
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
char *db_get_schema(db_handle db) {
|
|
|
|
|
if (!db || !db->conn) return strdup("Database not available");
|
|
|
|
|
struct json_object *result = NULL;
|
|
|
|
|
r_status_t status = db_execute(db,
|
|
|
|
|
"SELECT name, sql FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
|
|
|
&result);
|
|
|
|
|
if (status != R_SUCCESS || !result) {
|
|
|
|
|
return strdup("Failed to get schema");
|
|
|
|
|
}
|
|
|
|
|
char *schema = strdup(json_object_to_json_string_ext(result, JSON_C_TO_STRING_PRETTY));
|
|
|
|
|
json_object_put(result);
|
|
|
|
|
return schema;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_store_file_version(db_handle db, const char *path) {
|
|
|
|
|
if (!db || !db->conn || !path) return R_ERROR_INVALID_ARG;
|
|
|
|
|
FILE *fp = fopen(path, "r");
|
|
|
|
|
if (!fp) return R_ERROR_FILE_NOT_FOUND;
|
|
|
|
|
fseek(fp, 0, SEEK_END);
|
|
|
|
|
long size = ftell(fp);
|
|
|
|
|
rewind(fp);
|
|
|
|
|
if (size <= 0 || size > 1000000) {
|
|
|
|
|
fclose(fp);
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
char *content = malloc(size + 1);
|
|
|
|
|
if (!content) {
|
|
|
|
|
fclose(fp);
|
|
|
|
|
return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
size_t read_size = fread(content, 1, size, fp);
|
|
|
|
|
content[read_size] = '\0';
|
|
|
|
|
fclose(fp);
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT INTO file_versions (path, content) VALUES (%Q, %Q)",
|
|
|
|
|
path, content);
|
|
|
|
|
free(content);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_save_conversation(db_handle db, const char *session_key, const char *data) {
|
|
|
|
|
if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT OR REPLACE INTO conversations (session_key, data, updated_at) "
|
|
|
|
|
"VALUES (%Q, %Q, CURRENT_TIMESTAMP)",
|
|
|
|
|
session_key, data);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
2026-02-10 04:29:48 +01:00
|
|
|
long long db_get_conversation_age(db_handle db, const char *session_key) {
|
|
|
|
|
if (!db || !db->conn || !session_key) return -1;
|
|
|
|
|
const char *sql = "SELECT CAST((julianday('now') - julianday(updated_at)) * 86400 AS INTEGER) FROM conversations WHERE session_key = ?";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_bind_text(stmt, 1, session_key, -1, SQLITE_STATIC);
|
|
|
|
|
long long age = -1;
|
|
|
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
age = sqlite3_column_int64(stmt, 0);
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
return age;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_delete_conversation(db_handle db, const char *session_key) {
|
|
|
|
|
if (!db || !db->conn || !session_key) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf("DELETE FROM conversations WHERE session_key = %Q", session_key);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
2026-01-28 19:34:39 +01:00
|
|
|
r_status_t db_load_conversation(db_handle db, const char *session_key, char **data) {
|
|
|
|
|
if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG;
|
|
|
|
|
*data = NULL;
|
|
|
|
|
const char *sql = "SELECT data FROM conversations WHERE session_key = ?";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_bind_text(stmt, 1, session_key, -1, SQLITE_STATIC);
|
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
|
|
|
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
|
|
|
|
*data = val ? strdup(val) : NULL;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND;
|
|
|
|
|
}
|
2026-02-14 08:07:05 +01:00
|
|
|
r_status_t db_snapshot_create(db_handle db, const char *session_id, const char *description,
|
|
|
|
|
const char **paths, const char **contents, int file_count,
|
|
|
|
|
long long *snapshot_id_out) {
|
|
|
|
|
if (!db || !db->conn || !session_id || !paths || !contents || file_count <= 0 || !snapshot_id_out)
|
|
|
|
|
return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
if (sqlite3_exec(db->conn, "BEGIN TRANSACTION", NULL, NULL, &err_msg) != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)",
|
|
|
|
|
session_id, description ? description : "");
|
|
|
|
|
if (!sql) {
|
|
|
|
|
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
|
|
|
|
|
return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
long long sid = sqlite3_last_insert_rowid(db->conn);
|
|
|
|
|
for (int i = 0; i < file_count; i++) {
|
|
|
|
|
sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT INTO snapshot_files (snapshot_id, path, content) VALUES (%lld, %Q, %Q)",
|
|
|
|
|
sid, paths[i], contents[i]);
|
|
|
|
|
if (!sql) {
|
|
|
|
|
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
|
|
|
|
|
return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (sqlite3_exec(db->conn, "COMMIT", NULL, NULL, &err_msg) != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
*snapshot_id_out = sid;
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_snapshot_list(db_handle db, const char *session_id, struct json_object **result) {
|
|
|
|
|
if (!db || !db->conn || !session_id || !result) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"SELECT s.id, s.session_id, s.description, s.created_at, "
|
|
|
|
|
"(SELECT COUNT(*) FROM snapshot_files sf WHERE sf.snapshot_id = s.id) AS file_count "
|
|
|
|
|
"FROM snapshots s WHERE s.session_id = %Q ORDER BY s.created_at DESC",
|
|
|
|
|
session_id);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
r_status_t status = db_execute(db, sql, result);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_snapshot_get_files(db_handle db, long long snapshot_id, struct json_object **result) {
|
|
|
|
|
if (!db || !db->conn || !result) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"SELECT path, content FROM snapshot_files WHERE snapshot_id = %lld",
|
|
|
|
|
snapshot_id);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
r_status_t status = db_execute(db, sql, result);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_snapshot_ensure_live(db_handle db, const char *session_id,
|
|
|
|
|
const char *description, long long *snapshot_id_out) {
|
|
|
|
|
if (!db || !db->conn || !session_id || !snapshot_id_out) return R_ERROR_INVALID_ARG;
|
|
|
|
|
const char *select_sql = "SELECT id FROM snapshots WHERE session_id = ? LIMIT 1";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, select_sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_STATIC);
|
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
|
|
|
*snapshot_id_out = sqlite3_column_int64(stmt, 0);
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
char *update_sql = sqlite3_mprintf(
|
|
|
|
|
"UPDATE snapshots SET description = %Q WHERE id = %lld",
|
|
|
|
|
description ? description : "", *snapshot_id_out);
|
|
|
|
|
if (!update_sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
rc = sqlite3_exec(db->conn, update_sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(update_sql);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
char *insert_sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)",
|
|
|
|
|
session_id, description ? description : "");
|
|
|
|
|
if (!insert_sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
rc = sqlite3_exec(db->conn, insert_sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(insert_sql);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
*snapshot_id_out = sqlite3_last_insert_rowid(db->conn);
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
r_status_t db_snapshot_upsert_file(db_handle db, long long snapshot_id,
|
|
|
|
|
const char *path, const char *content) {
|
|
|
|
|
if (!db || !db->conn || !path || !content) return R_ERROR_INVALID_ARG;
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT OR REPLACE INTO snapshot_files (snapshot_id, path, content) "
|
|
|
|
|
"VALUES (%lld, %Q, %Q)",
|
|
|
|
|
snapshot_id, path, content);
|
|
|
|
|
if (!sql) return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
sqlite3_free(sql);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
|
|
|
|
|
}
|