#define _GNU_SOURCE #include "sqlite_db_ops.h" #include "debug.h" #include "config.h" #include #include #include #include // Database handle owned by sqlite_db_ops. static sqlite3* g_db = NULL; extern char g_database_path[512]; // Optional per-thread connection override (used by thread pool workers) static __thread sqlite3* g_thread_db = NULL; static sqlite3* sqlite_db_active_connection(void) { return g_thread_db ? g_thread_db : g_db; } typedef struct sqlite_db_stmt { sqlite3_stmt* stmt; } sqlite_db_stmt_t; int sqlite_db_init(const char* connection_string) { if (!connection_string || connection_string[0] == '\0') return DB_MISUSE; if (g_db) return DB_OK; int rc = sqlite3_open(connection_string, &g_db); if (rc != SQLITE_OK) { if (g_db) { sqlite3_close(g_db); g_db = NULL; } return DB_ERROR; } strncpy(g_database_path, connection_string, sizeof(g_database_path) - 1); g_database_path[sizeof(g_database_path) - 1] = '\0'; return DB_OK; } void sqlite_db_close(void) { if (!g_db) return; sqlite3_close(g_db); g_db = NULL; } int sqlite_db_set_thread_connection(void* connection) { g_thread_db = (sqlite3*)connection; return DB_OK; } void sqlite_db_clear_thread_connection(void) { g_thread_db = NULL; } int sqlite_db_open_worker_connection(const char* sqlite_db_path, void** out_connection) { if (!out_connection) return -1; *out_connection = NULL; const char* effective_path = (sqlite_db_path && sqlite_db_path[0] != '\0') ? sqlite_db_path : g_database_path; if (!effective_path || effective_path[0] == '\0') return -1; sqlite3* db = NULL; int rc = sqlite3_open_v2(effective_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc != SQLITE_OK) { if (db) sqlite3_close(db); return -1; } sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); sqlite3_exec(db, "PRAGMA wal_autocheckpoint=0;", NULL, NULL, NULL); sqlite3_busy_timeout(db, 5000); sqlite_db_set_thread_connection(db); *out_connection = (void*)db; return 0; } void sqlite_db_close_worker_connection(void* connection) { if (!connection) return; sqlite_db_clear_thread_connection(); sqlite3_close((sqlite3*)connection); } int sqlite_db_is_available(void) { return sqlite_db_active_connection() != NULL; } const char* sqlite_db_last_error(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return "database not available"; return sqlite3_errmsg(db); } const char* sqlite_db_get_database_path(void) { return g_database_path; } int sqlite_db_prepare(const char* sql, sqlite_db_stmt_t** out_stmt) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sql || !out_stmt) return DB_MISUSE; sqlite3_stmt* raw_stmt = NULL; int rc = sqlite3_prepare_v2(db, sql, -1, &raw_stmt, NULL); if (rc != SQLITE_OK) return rc; sqlite_db_stmt_t* wrapper = (sqlite_db_stmt_t*)malloc(sizeof(sqlite_db_stmt_t)); if (!wrapper) { sqlite3_finalize(raw_stmt); return DB_ERROR; } wrapper->stmt = raw_stmt; *out_stmt = wrapper; return DB_OK; } int sqlite_db_bind_text_param(sqlite_db_stmt_t* stmt, int index, const char* value) { if (!stmt || !stmt->stmt) return DB_MISUSE; return sqlite3_bind_text(stmt->stmt, index, value ? value : "", -1, SQLITE_TRANSIENT); } int sqlite_db_bind_int_param(sqlite_db_stmt_t* stmt, int index, int value) { if (!stmt || !stmt->stmt) return DB_MISUSE; return sqlite3_bind_int(stmt->stmt, index, value); } int sqlite_db_bind_int64_param(sqlite_db_stmt_t* stmt, int index, long long value) { if (!stmt || !stmt->stmt) return DB_MISUSE; return sqlite3_bind_int64(stmt->stmt, index, (sqlite3_int64)value); } int sqlite_db_step_stmt(sqlite_db_stmt_t* stmt) { if (!stmt || !stmt->stmt) return DB_MISUSE; return sqlite3_step(stmt->stmt); } int sqlite_db_reset_stmt(sqlite_db_stmt_t* stmt) { if (!stmt || !stmt->stmt) return DB_MISUSE; return sqlite3_reset(stmt->stmt); } const char* sqlite_db_column_text_value(sqlite_db_stmt_t* stmt, int col) { if (!stmt || !stmt->stmt) return NULL; return (const char*)sqlite3_column_text(stmt->stmt, col); } int sqlite_db_column_int_value(sqlite_db_stmt_t* stmt, int col) { if (!stmt || !stmt->stmt) return 0; return sqlite3_column_int(stmt->stmt, col); } long long sqlite_db_column_int64_value(sqlite_db_stmt_t* stmt, int col) { if (!stmt || !stmt->stmt) return 0; return (long long)sqlite3_column_int64(stmt->stmt, col); } double sqlite_db_column_double_value(sqlite_db_stmt_t* stmt, int col) { if (!stmt || !stmt->stmt) return 0.0; return sqlite3_column_double(stmt->stmt, col); } void sqlite_db_finalize_stmt(sqlite_db_stmt_t* stmt) { if (!stmt) return; if (stmt->stmt) sqlite3_finalize(stmt->stmt); free(stmt); } int sqlite_db_log_subscription_created(const char* sub_id, const char* wsi_ptr, const char* client_ip, const char* filter_json) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sub_id || !wsi_ptr || !client_ip) return -1; const char* sql = "INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) " "VALUES (?, ?, ?, 'created', ?)"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, wsi_ptr, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_log_subscription_closed(const char* sub_id, const char* client_ip) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sub_id) return -1; const char* insert_sql = "INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) " "VALUES (?, '', ?, 'closed')"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } const char* update_sql = "UPDATE subscriptions " "SET ended_at = strftime('%s', 'now') " "WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL"; if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_log_subscription_disconnected(const char* client_ip) { sqlite3* db = sqlite_db_active_connection(); if (!db || !client_ip) return -1; const char* update_sql = "UPDATE subscriptions " "SET ended_at = strftime('%s', 'now') " "WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); int changes = sqlite3_changes(db); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) return -1; if (changes > 0) { const char* insert_sql = "INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) " "VALUES ('disconnect', '', ?, 'disconnected')"; if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } return changes; } int sqlite_db_update_subscription_events_sent(const char* sub_id, int events_sent) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sub_id) return -1; const char* sql = "UPDATE subscriptions " "SET events_sent = ? " "WHERE subscription_id = ? AND event_type = 'created'"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(stmt, 1, events_sent); sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_cleanup_orphaned_subscriptions(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return -1; const char* sql = "UPDATE subscriptions " "SET ended_at = strftime('%s', 'now') " "WHERE event_type = 'created' AND ended_at IS NULL"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; int rc = sqlite3_step(stmt); int changes = sqlite3_changes(db); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? changes : -1; } int sqlite_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1; const char* sql = "SELECT pubkey FROM events WHERE id = ?"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); if (pubkey) { snprintf(pubkey_out, pubkey_out_size, "%s", pubkey); sqlite3_finalize(stmt); return 1; } } sqlite3_finalize(stmt); return 0; } int sqlite_db_delete_event_by_id(const char* event_id, const char* requester_pubkey) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event_id || !requester_pubkey) return -1; const char* sql = "DELETE FROM events WHERE id = ? AND pubkey = ?"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, requester_pubkey, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); int changes = sqlite3_changes(db); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) return -1; return changes; } int sqlite_db_delete_events_by_address(const char* pubkey, int kind, const char* d_tag, long before_timestamp) { sqlite3* db = sqlite_db_active_connection(); if (!db || !pubkey) return -1; const char* sql_with_d = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? " "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'"; const char* sql_no_d = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?"; const int has_d = (d_tag && d_tag[0] != '\0'); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_int(stmt, 1, kind); sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 3, before_timestamp); if (has_d) { sqlite3_bind_text(stmt, 4, d_tag, -1, SQLITE_TRANSIENT); } int rc = sqlite3_step(stmt); int changes = sqlite3_changes(db); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) return -1; return changes; } static int sqlite_db_scalar_exists_open(sqlite3* db, const char* sql, const char* value) { sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; if (value) sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT); int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; sqlite3_finalize(stmt); return exists; } int sqlite_db_is_pubkey_blacklisted(const char* pubkey) { if (!pubkey || g_database_path[0] == '\0') return 0; sqlite3* db = NULL; if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; const char* sql = "SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' " "AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"; int exists = sqlite_db_scalar_exists_open(db, sql, pubkey); sqlite3_close(db); return exists; } int sqlite_db_is_hash_blacklisted(const char* resource_hash) { if (!resource_hash || g_database_path[0] == '\0') return 0; sqlite3* db = NULL; if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; const char* sql = "SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' " "AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1"; int exists = sqlite_db_scalar_exists_open(db, sql, resource_hash); sqlite3_close(db); return exists; } int sqlite_db_is_pubkey_whitelisted(const char* pubkey) { if (!pubkey || g_database_path[0] == '\0') return 0; sqlite3* db = NULL; if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; const char* sql = "SELECT 1 FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') " "AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"; int exists = sqlite_db_scalar_exists_open(db, sql, pubkey); sqlite3_close(db); return exists; } int sqlite_db_count_active_whitelist_rules(void) { if (g_database_path[0] == '\0') return 0; sqlite3* db = NULL; sqlite3_stmt* stmt = NULL; int count = 0; if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') " "AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); } sqlite3_close(db); return count; } int sqlite_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sql || !out_count) return -1; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } for (int i = 0; i < bind_param_count; i++) { const char* param = (bind_params && bind_params[i]) ? bind_params[i] : ""; sqlite3_bind_text(stmt, i + 1, param, -1, SQLITE_TRANSIENT); } int count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); *out_count = count; return 0; } char* sqlite_db_execute_readonly_query_json(const char* query, const char* request_id, char* error_message, size_t error_size, int max_rows, int timeout_ms) { sqlite3* db = sqlite_db_active_connection(); if (!db || !query || !request_id || !error_message) return NULL; sqlite3_busy_timeout(db, timeout_ms > 0 ? timeout_ms : 5000); sqlite3_stmt* stmt = NULL; int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL); if (rc != SQLITE_OK) { const char* err_msg = sqlite3_errmsg(db); snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg ? err_msg : "unknown"); return NULL; } cJSON* response = cJSON_CreateObject(); if (!response) { sqlite3_finalize(stmt); snprintf(error_message, error_size, "Failed to allocate query response object"); return NULL; } cJSON_AddStringToObject(response, "query_type", "sql_query"); cJSON_AddStringToObject(response, "request_id", request_id); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON_AddStringToObject(response, "query", query); int col_count = sqlite3_column_count(stmt); cJSON* columns = cJSON_CreateArray(); if (!columns) { sqlite3_finalize(stmt); cJSON_Delete(response); snprintf(error_message, error_size, "Failed to allocate columns array"); return NULL; } for (int i = 0; i < col_count; i++) { const char* col_name = sqlite3_column_name(stmt, i); cJSON_AddItemToArray(columns, cJSON_CreateString(col_name ? col_name : "")); } cJSON_AddItemToObject(response, "columns", columns); cJSON* rows = cJSON_CreateArray(); if (!rows) { sqlite3_finalize(stmt); cJSON_Delete(response); snprintf(error_message, error_size, "Failed to allocate rows array"); return NULL; } const int row_limit = (max_rows > 0) ? max_rows : 1000; int row_count = 0; struct timespec start_time; clock_gettime(CLOCK_MONOTONIC, &start_time); while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && row_count < row_limit) { cJSON* row = cJSON_CreateArray(); if (!row) { sqlite3_finalize(stmt); cJSON_Delete(rows); cJSON_Delete(response); snprintf(error_message, error_size, "Failed to allocate row array"); return NULL; } for (int i = 0; i < col_count; i++) { int col_type = sqlite3_column_type(stmt, i); switch (col_type) { case SQLITE_INTEGER: cJSON_AddItemToArray(row, cJSON_CreateNumber((double)sqlite3_column_int64(stmt, i))); break; case SQLITE_FLOAT: cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_double(stmt, i))); break; case SQLITE_TEXT: { const char* text = (const char*)sqlite3_column_text(stmt, i); cJSON_AddItemToArray(row, cJSON_CreateString(text ? text : "")); break; } case SQLITE_BLOB: { const void* blob = sqlite3_column_blob(stmt, i); int blob_size = sqlite3_column_bytes(stmt, i); if (blob && blob_size > 0) { char* hex_str = malloc((size_t)blob_size * 2 + 1); if (hex_str) { for (int j = 0; j < blob_size; j++) { sprintf(hex_str + j * 2, "%02x", ((const unsigned char*)blob)[j]); } hex_str[(size_t)blob_size * 2] = '\0'; cJSON_AddItemToArray(row, cJSON_CreateString(hex_str)); free(hex_str); } else { cJSON_AddItemToArray(row, cJSON_CreateString("[BLOB]")); } } else { cJSON_AddItemToArray(row, cJSON_CreateString("")); } break; } case SQLITE_NULL: cJSON_AddItemToArray(row, cJSON_CreateNull()); break; default: cJSON_AddItemToArray(row, cJSON_CreateString("[UNKNOWN]")); break; } } cJSON_AddItemToArray(rows, row); row_count++; struct timespec current_time; clock_gettime(CLOCK_MONOTONIC, ¤t_time); double elapsed = (current_time.tv_sec - start_time.tv_sec) + (current_time.tv_nsec - start_time.tv_nsec) / 1e9; if (elapsed > 4.5) { break; } } sqlite3_finalize(stmt); if (rc != SQLITE_DONE && rc != SQLITE_ROW) { const char* err_msg = sqlite3_errmsg(db); snprintf(error_message, error_size, "SQL execution failed: %s", err_msg ? err_msg : "unknown"); cJSON_Delete(rows); cJSON_Delete(response); return NULL; } if (row_count >= row_limit) { cJSON_AddStringToObject(response, "warning", "Result truncated to maximum row limit"); } cJSON_AddNumberToObject(response, "row_count", row_count); cJSON_AddNumberToObject(response, "execution_time_ms", 0); cJSON_AddItemToObject(response, "rows", rows); char* json_result = cJSON_Print(response); cJSON_Delete(response); if (!json_result) { snprintf(error_message, error_size, "Failed to generate JSON response"); return NULL; } return json_result; } int sqlite_db_get_total_event_count_ll(long long* out_count) { sqlite3* db = sqlite_db_active_connection(); if (!db || !out_count) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT COUNT(*) FROM events"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; long long count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int64(stmt, 0); } sqlite3_finalize(stmt); *out_count = count; return 0; } int sqlite_db_get_event_count_since(time_t cutoff, long long* out_count) { sqlite3* db = sqlite_db_active_connection(); if (!db || !out_count) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff); long long count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int64(stmt, 0); } sqlite3_finalize(stmt); *out_count = count; return 0; } cJSON* sqlite_db_get_event_kind_distribution_rows(long long* out_total_events) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; cJSON* rows = cJSON_CreateArray(); if (!rows) { sqlite3_finalize(stmt); return NULL; } long long total = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* row = cJSON_CreateObject(); if (!row) { sqlite3_finalize(stmt); cJSON_Delete(rows); return NULL; } int kind = sqlite3_column_int(stmt, 0); long long count = sqlite3_column_int64(stmt, 1); total += count; cJSON_AddNumberToObject(row, "kind", kind); cJSON_AddNumberToObject(row, "count", count); cJSON_AddItemToArray(rows, row); } sqlite3_finalize(stmt); if (out_total_events) { *out_total_events = total; } return rows; } cJSON* sqlite_db_get_top_pubkeys_rows(int limit) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10); cJSON* rows = cJSON_CreateArray(); if (!rows) { sqlite3_finalize(stmt); return NULL; } while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* row = cJSON_CreateObject(); if (!row) { sqlite3_finalize(stmt); cJSON_Delete(rows); return NULL; } const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); long long count = sqlite3_column_int64(stmt, 1); cJSON_AddStringToObject(row, "pubkey", pubkey ? pubkey : ""); cJSON_AddNumberToObject(row, "count", count); cJSON_AddItemToArray(rows, row); } sqlite3_finalize(stmt); return rows; } cJSON* sqlite_db_get_subscription_details_rows(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT * " "FROM active_subscriptions_log " "ORDER BY created_at DESC"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; cJSON* rows = cJSON_CreateArray(); if (!rows) { sqlite3_finalize(stmt); return NULL; } while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* row = cJSON_CreateObject(); if (!row) { sqlite3_finalize(stmt); cJSON_Delete(rows); return NULL; } const char* sub_id = (const char*)sqlite3_column_text(stmt, 0); const char* client_ip = (const char*)sqlite3_column_text(stmt, 1); const char* filter_json = (const char*)sqlite3_column_text(stmt, 2); long long events_sent = sqlite3_column_int64(stmt, 3); long long created_at = sqlite3_column_int64(stmt, 4); long long duration_seconds = sqlite3_column_int64(stmt, 5); const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6); cJSON_AddStringToObject(row, "id", sub_id ? sub_id : ""); cJSON_AddStringToObject(row, "client_ip", client_ip ? client_ip : ""); cJSON_AddStringToObject(row, "filter_json", filter_json ? filter_json : "[]"); cJSON_AddNumberToObject(row, "events_sent", events_sent); cJSON_AddNumberToObject(row, "created_at", created_at); cJSON_AddNumberToObject(row, "duration_seconds", duration_seconds); cJSON_AddStringToObject(row, "wsi_pointer", wsi_pointer ? wsi_pointer : ""); cJSON_AddItemToArray(rows, row); } sqlite3_finalize(stmt); return rows; } cJSON* sqlite_db_get_all_config_rows(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT key, value FROM config ORDER BY key"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; cJSON* rows = cJSON_CreateArray(); if (!rows) { sqlite3_finalize(stmt); return NULL; } while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* row = cJSON_CreateObject(); if (!row) { sqlite3_finalize(stmt); cJSON_Delete(rows); return NULL; } const char* key = (const char*)sqlite3_column_text(stmt, 0); const char* value = (const char*)sqlite3_column_text(stmt, 1); cJSON_AddStringToObject(row, "key", key ? key : ""); cJSON_AddStringToObject(row, "value", value ? value : ""); cJSON_AddItemToArray(rows, row); } sqlite3_finalize(stmt); return rows; } char* sqlite_db_get_config_value_dup(const char* key) { sqlite3* db = sqlite_db_active_connection(); if (!db || !key) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT value FROM config WHERE key = ?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); char* result = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); if (value) result = strdup(value); } sqlite3_finalize(stmt); return result; } int sqlite_db_set_config_value_full(const char* key, const char* value, const char* data_type, const char* description, const char* category, int requires_restart) { sqlite3* db = sqlite_db_active_connection(); if (!db || !key || !value || !data_type) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " "VALUES (?, ?, ?, ?, ?, ?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 6, requires_restart); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_update_config_value_only(const char* key, const char* value) { sqlite3* db = sqlite_db_active_connection(); if (!db || !key || !value) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_upsert_config_value(const char* key, const char* value, const char* data_type) { sqlite3* db = sqlite_db_active_connection(); if (!db || !key || !value || !data_type) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_store_relay_private_key_hex(const char* relay_privkey_hex) { sqlite3* db = sqlite_db_active_connection(); if (!db || !relay_privkey_hex) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } char* sqlite_db_get_relay_private_key_hex_dup(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT private_key_hex FROM relay_seckey"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; char* result = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0); if (key_from_db) result = strdup(key_from_db); } sqlite3_finalize(stmt); return result; } int sqlite_db_store_config_event(const cJSON* event) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event) return -1; cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id"); cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey"); cJSON* created_at_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "created_at"); cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "kind"); cJSON* content_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "content"); cJSON* sig_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "sig"); cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "tags"); if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) { return -1; } char* tags_str = cJSON_Print(tags_obj); if (!tags_str) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { free(tags_str); return -1; } sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); free(tags_str); return (rc == SQLITE_DONE) ? 0 : -1; } static int sqlite_db_insert_event_tags_json_with_db(sqlite3* db, const char* event_id, const char* tags_json) { if (!db || !event_id || !tags_json) { return -1; } cJSON* tags = cJSON_Parse(tags_json); if (!tags || !cJSON_IsArray(tags)) { if (tags) cJSON_Delete(tags); return 0; } const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { cJSON_Delete(tags); return -1; } int tag_index = 0; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* name = cJSON_GetArrayItem(tag, 0); cJSON* value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(name) && cJSON_IsString(value)) { sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 4, tag_index); if (sqlite3_step(stmt) != SQLITE_DONE) { DEBUG_WARN("Failed to insert event tag for %s: %s", event_id, sqlite3_errmsg(db)); } } } tag_index++; } sqlite3_finalize(stmt); cJSON_Delete(tags); return 0; } int sqlite_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at, int kind, const char* event_type, const char* content, const char* sig, const char* tags_json, const char* event_json, int* out_step_rc, int* out_extended_errcode) { sqlite3* db = sqlite_db_active_connection(); if (!db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) { return -1; } const char* sql = "INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags, event_json) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { 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, (sqlite3_int64)created_at); sqlite3_bind_int(stmt, 4, kind); sqlite3_bind_text(stmt, 5, event_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, content, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 7, sig, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT); int step_rc = sqlite3_step(stmt); int extended_errcode = sqlite3_extended_errcode(db); sqlite3_finalize(stmt); if (step_rc == SQLITE_DONE) { (void)sqlite_db_insert_event_tags_json_with_db(db, id, tags_json); } if (out_step_rc) { *out_step_rc = step_rc; } if (out_extended_errcode) { *out_extended_errcode = extended_errcode; } return 0; } int sqlite_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) { sqlite3* db = sqlite_db_active_connection(); if (!db || !out_min_created_at || !out_max_created_at) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } long long min_ts = 0; long long max_ts = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { min_ts = sqlite3_column_int64(stmt, 0); max_ts = sqlite3_column_int64(stmt, 1); } sqlite3_finalize(stmt); *out_min_created_at = min_ts; *out_max_created_at = max_ts; return 0; } int sqlite_db_event_id_exists(const char* event_id, int* out_exists) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event_id || !out_exists) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC); int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; sqlite3_finalize(stmt); *out_exists = exists; return 0; } cJSON* sqlite_db_retrieve_event_by_id(const char* event_id) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event_id) return NULL; const char* sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return NULL; } sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); cJSON* event = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { event = cJSON_CreateObject(); if (event) { cJSON_AddStringToObject(event, "id", (const char*)sqlite3_column_text(stmt, 0)); cJSON_AddStringToObject(event, "pubkey", (const char*)sqlite3_column_text(stmt, 1)); cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); cJSON_AddStringToObject(event, "content", (const char*)sqlite3_column_text(stmt, 4)); cJSON_AddStringToObject(event, "sig", (const char*)sqlite3_column_text(stmt, 5)); const char* tags_json = (const char*)sqlite3_column_text(stmt, 6); if (tags_json) { cJSON* tags = cJSON_Parse(tags_json); if (tags) { cJSON_AddItemToObject(event, "tags", tags); } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } } sqlite3_finalize(stmt); return event; } char* sqlite_db_get_latest_event_pubkey_for_kind_dup(int kind) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; const char* sql = "SELECT pubkey FROM events WHERE kind = ? ORDER BY created_at DESC LIMIT 1"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return NULL; } sqlite3_bind_int(stmt, 1, kind); char* pubkey_dup = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); if (pubkey) { pubkey_dup = strdup(pubkey); } } sqlite3_finalize(stmt); return pubkey_dup; } int sqlite_db_get_config_row_count(int* out_count) { sqlite3* db = sqlite_db_active_connection(); if (!db || !out_count) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "SELECT COUNT(*) FROM config"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } int count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); *out_count = count; return 0; } int sqlite_db_store_event_tags_cjson(const char* event_id, const cJSON* tags) { sqlite3* db = sqlite_db_active_connection(); if (!db || !event_id || !tags || !cJSON_IsArray(tags)) { return 0; // Not an error if no tags } const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)"; sqlite3_stmt* stmt = NULL; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(db)); return -1; } int tag_index = 0; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* name = cJSON_GetArrayItem(tag, 0); cJSON* value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(name) && cJSON_IsString(value)) { sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 4, tag_index); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(db)); } } } tag_index++; } sqlite3_finalize(stmt); return 0; } int sqlite_db_populate_event_tags_from_existing(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return -1; sqlite3_stmt* check_stmt = NULL; if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) { return -1; } if (sqlite3_step(check_stmt) == SQLITE_ROW && sqlite3_column_int(check_stmt, 0) > 0) { sqlite3_finalize(check_stmt); DEBUG_INFO("event_tags already populated, skipping"); return 0; } sqlite3_finalize(check_stmt); DEBUG_INFO("Populating event_tags from existing events..."); const char* sql = "SELECT id, tags FROM events WHERE tags != '[]'"; sqlite3_stmt* stmt = NULL; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) return -1; sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL); int event_count = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { const char* event_id = (const char*)sqlite3_column_text(stmt, 0); const char* tags_json = (const char*)sqlite3_column_text(stmt, 1); if (event_id && tags_json) { cJSON* tags = cJSON_Parse(tags_json); if (tags) { sqlite_db_store_event_tags_cjson(event_id, tags); cJSON_Delete(tags); event_count++; } } } sqlite3_finalize(stmt); sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); DEBUG_INFO("Populated event_tags for %d events", event_count); return 0; } int sqlite_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { sqlite3* db = sqlite_db_active_connection(); if (!db || !rule_type || !pattern_type || !pattern_value) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { sqlite3* db = sqlite_db_active_connection(); if (!db || !rule_type || !pattern_type || !pattern_value) return -1; sqlite3_stmt* stmt = NULL; const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } int sqlite_db_delete_wot_whitelist_rules(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return -1; const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'"; int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); return (rc == SQLITE_OK) ? 0 : -1; } int sqlite_db_count_wot_whitelist_rules(void) { if (!sqlite_db_is_available()) return 0; const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; int count = 0; if (sqlite_db_count_with_sql(sql, NULL, 0, &count) != 0) { return 0; } return count; } int sqlite_db_table_exists(const char* table_name, int* out_exists) { sqlite3* db = sqlite_db_active_connection(); if (!db || !table_name || !out_exists) return -1; const char* sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, table_name, -1, SQLITE_TRANSIENT); *out_exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; sqlite3_finalize(stmt); return 0; } char* sqlite_db_get_schema_version_dup(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return NULL; const char* sql = "SELECT value FROM schema_info WHERE key = 'version'"; sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return NULL; } char* result = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* version = (const char*)sqlite3_column_text(stmt, 0); if (version) { result = strdup(version); } } sqlite3_finalize(stmt); return result; } int sqlite_db_exec_sql(const char* sql) { sqlite3* db = sqlite_db_active_connection(); if (!db || !sql) return -1; int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); return (rc == SQLITE_OK) ? 0 : -1; } int sqlite_db_wal_checkpoint_passive(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return -1; int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL); return (rc == SQLITE_OK) ? 0 : -1; } int sqlite_db_wal_checkpoint_truncate(void) { sqlite3* db = sqlite_db_active_connection(); if (!db) return -1; int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL); return (rc == SQLITE_OK) ? 0 : -1; }