// 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);
char *expanded = malloc(home_len + path_len + 1);
if (!expanded) return NULL;
memcpy(expanded, home_dir, home_len);
memcpy(expanded + home_len, path + 1, path_len);
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"
");"
"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"
");"
"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"
");"
"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);";
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;
}
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;
}
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;
}
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;
}