|
// 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;
|
|
}
|