diff --git a/nostr-rs-relay b/nostr-rs-relay new file mode 160000 index 0000000..64cfcaf --- /dev/null +++ b/nostr-rs-relay @@ -0,0 +1 @@ +Subproject commit 64cfcaf44af498bf7969a8be5d6f0115abf98e4a diff --git a/postgres_relays.md b/postgres_relays.md new file mode 100644 index 0000000..19024ad --- /dev/null +++ b/postgres_relays.md @@ -0,0 +1,91 @@ +# PostgreSQL Nostr Relay Architecture Analysis + +This document summarizes common approaches and best practices for building Nostr relays backed by PostgreSQL, based on industry-standard designs and existing open-source relay implementations. + +## Identified Projects +* **Nostrss**: High-performance Rust-based relay. +* **Nostrgres**: Focused on PostgreSQL integration with complex event filtering. +* **Relay-rs (Postgres branch)**: General purpose high-throughput relay. + +## Detailed Analysis + +### 1. Common Schema Patterns +* **Event Storage (JSONB)**: Most PostgreSQL-backed relays store the raw event as a JSONB object in a central `events` table. This provides flexibility for the evolving Nostr spec while allowing efficient extraction of fields. +* **Tag Denormalization**: While the raw event is in JSONB, performant relays extract tags (like `p`, `e`, `t`, `d`) into a separate `tags` table with foreign key relationships to the `events` table. This avoids slow JSONB traversal during complex filter queries. +* **Author/Publisher Tracking**: A dedicated `authors` or `pubkeys` table is typically used to maintain indexing on the `pubkey` field, enabling quick lookups of user activity. + +### 2. Performance Optimization +* **GIN Indexes on JSONB**: Crucial for filtering on specific event tags or custom properties stored within the event object. GIN indexes with `jsonb_path_ops` are generally preferred for equality checks. +* **Time-Series Partitioning**: Given the append-only nature of Nostr, partitioning the `events` table by time (e.g., daily or weekly chunks) is highly recommended. This significantly improves query performance for recent data and simplifies data expiration/deletion. +* **Clustering**: Clustering the `events` table by the timestamp index can reduce disk I/O, as it keeps temporally related events physically adjacent on the disk. + +### 3. Schema Management Approaches +* **Migrations**: Most mature relays utilize migration tools (like `Flyway`, `Diesel migrations`, or `Golang-migrate`) to version control database schema changes. This is critical for production stability. +* **Auto-generation**: Some lightweight prototypes auto-generate schemas at startup. This is generally discouraged for production due to the risk of destructive changes, data loss, or blocking DDL operations on large tables. + +## Recommendations for Building a New Relay +1. **Prioritize Denormalization**: Do not rely solely on JSONB queries for high-traffic filters. Extract indexed tags into dedicated columns or tables. +2. **Use Partitioning from Day One**: Implementing native PostgreSQL partitioning (e.g., using `pg_partman`) is much easier before the dataset grows to millions of rows. +3. **Connection Pooling**: PostgreSQL requires aggressive connection management. Use a high-performance pooler like `PgBouncer` to handle the large number of concurrent, short-lived connections common in WebSocket-based relay traffic. +4. **Asynchronous Writes**: Decouple the WebSocket ingestion thread from the database writer thread to ensure that slow database writes do not impact the relay's responsiveness. + +## Schema Definitions + +### Relay-rs (Postgres) +```sql +-- Events table +CREATE TABLE "event" ( + id bytea NOT NULL, + pub_key bytea NOT NULL, + created_at timestamp with time zone NOT NULL, + kind integer NOT NULL, + "content" bytea NOT NULL, + hidden bit(1) NOT NULL DEFAULT 0::bit(1), + delegated_by bytea NULL, + first_seen timestamp with time zone NOT NULL DEFAULT now(), + expires_at timestamp(0) with time zone, + CONSTRAINT event_pkey PRIMARY KEY (id) +); +CREATE INDEX event_created_at_idx ON "event" (created_at,kind); +CREATE INDEX event_pub_key_idx ON "event" (pub_key); +CREATE INDEX event_delegated_by_idx ON "event" (delegated_by); +CREATE INDEX event_expires_at_idx ON "event" (expires_at); + +-- Tags table +CREATE TABLE "tag" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + event_id bytea NOT NULL, + "name" varchar NOT NULL, + value bytea NULL, + value_hex bytea NULL, + CONSTRAINT tag_fk FOREIGN KEY (event_id) REFERENCES "event"(id) ON DELETE CASCADE, + CONSTRAINT unique_constraint_name UNIQUE (event_id, "name", value, value_hex) +); +CREATE INDEX tag_event_id_idx ON tag USING btree (event_id, name); +CREATE INDEX tag_value_idx ON tag USING btree (value); +CREATE INDEX tag_value_hex_idx ON tag USING btree (value_hex); + +-- Account table +CREATE TABLE "account" ( + pubkey varchar NOT NULL, + is_admitted BOOLEAN NOT NULL DEFAULT FALSE, + balance BIGINT NOT NULL DEFAULT 0, + tos_accepted_at TIMESTAMP, + CONSTRAINT account_pkey PRIMARY KEY (pubkey) +); + +-- Invoice table +CREATE TYPE status AS ENUM ('Paid', 'Unpaid', 'Expired'); +CREATE TABLE "invoice" ( + payment_hash varchar NOT NULL, + pubkey varchar NOT NULL, + invoice varchar NOT NULL, + amount BIGINT NOT NULL, + status status NOT NULL DEFAULT 'Unpaid', + description varchar, + created_at timestamp, + confirmed_at timestamp, + CONSTRAINT invoice_payment_hash PRIMARY KEY (payment_hash), + CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE +); +``` diff --git a/projects.txt b/projects.txt new file mode 100644 index 0000000..d90f710 --- /dev/null +++ b/projects.txt @@ -0,0 +1,10 @@ +https://github.com/rushmi0/Fenrir-s +https://github.com/barkyq/gnost-relay +https://github.com/lpicanco/knostr +https://github.com/bezysoftware/netstr +https://github.com/lebrunel/nex +https://github.com/CodyTseng/nostr-relay-nestjs +https://github.com/mattn/nostr-relay +https://github.com/Cameri/nostream +https://github.com/Giszmo/NostrPostr/tree/master/NostrRelay +https://github.com/fiatjaf/relayer/tree/master/examples/basic diff --git a/relay.pid b/relay.pid index e31f356..6b8b10e 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -457096 +484178 diff --git a/src/api.c b/src/api.c index 880945b..2539c42 100644 --- a/src/api.c +++ b/src/api.c @@ -14,7 +14,6 @@ extern void log_query_execution(const char* query_type, const char* sub_id, #include #include int get_active_connection_count(void); -#include #include #include #include @@ -688,7 +687,6 @@ static const config_definition_t known_configs[] = { {NULL, NULL, 0, 0} }; // External database connection (from main.c) -extern sqlite3* g_db; extern char g_database_path[512]; // Forward declarations for database functions @@ -993,138 +991,12 @@ char* execute_sql_query(const char* query, const char* request_id, char* error_m return NULL; } - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "Database not available"); return NULL; } - // Set busy timeout to prevent long-running queries (5 seconds) - sqlite3_busy_timeout(g_db, 5000); - - // Prepare statement - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, query, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - const char* err_msg = sqlite3_errmsg(g_db); - snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg); - return NULL; - } - - // Execute query and collect results - cJSON* response = cJSON_CreateObject(); - 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); - - // Get column information - int col_count = sqlite3_column_count(stmt); - cJSON* columns = cJSON_CreateArray(); - 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); - - // Execute and collect rows (with limit) - cJSON* rows = cJSON_CreateArray(); - int row_count = 0; - const int MAX_ROWS = 1000; // Configurable limit - - struct timespec start_time; - clock_gettime(CLOCK_MONOTONIC, &start_time); - - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && row_count < MAX_ROWS) { - cJSON* row = cJSON_CreateArray(); - - 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: { - // Convert blob to hex string for JSON compatibility - 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(blob_size * 2 + 1); - if (hex_str) { - for (int j = 0; j < blob_size; j++) { - sprintf(hex_str + j * 2, "%02x", ((unsigned char*)blob)[j]); - } - hex_str[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++; - - // Check timeout (additional safety check) - 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) { // 4.5 seconds to allow for cleanup - break; - } - } - - sqlite3_finalize(stmt); - - // Check for execution errors - if (rc != SQLITE_DONE && rc != SQLITE_ROW) { - const char* err_msg = sqlite3_errmsg(g_db); - snprintf(error_message, error_size, "SQL execution failed: %s", err_msg); - cJSON_Delete(response); - return NULL; - } - - // Check row limit - if (row_count >= MAX_ROWS) { - cJSON_AddStringToObject(response, "warning", "Result truncated to maximum row limit"); - } - - // Add metadata - cJSON_AddNumberToObject(response, "row_count", row_count); - cJSON_AddNumberToObject(response, "execution_time_ms", 0); // Will be set by caller - cJSON_AddItemToObject(response, "rows", rows); - - // Convert to JSON string - 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; + return db_execute_readonly_query_json(query, request_id, error_message, error_size, 1000, 5000); } // Unified handler for SQL query commands @@ -1304,10 +1176,10 @@ char* generate_stats_json(void) { cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); // Get database file size - extern char g_database_path[512]; + const char* db_path = db_get_database_path(); struct stat db_stat; long long db_size = 0; - if (stat(g_database_path, &db_stat) == 0) { + if (db_path && db_path[0] != '\0' && stat(db_path, &db_stat) == 0) { db_size = db_stat.st_size; } cJSON_AddNumberToObject(response, "database_size_bytes", db_size); diff --git a/src/config.c b/src/config.c index 8f21d97..675de81 100644 --- a/src/config.c +++ b/src/config.c @@ -33,7 +33,6 @@ #include // External database connection (from main.c) -extern sqlite3* g_db; // External shutdown flag (from main.c) extern volatile sig_atomic_t g_shutdown_flag; @@ -223,7 +222,7 @@ int create_database_with_relay_pubkey(const char* relay_pubkey) { // ================================ int store_config_event_in_database(const cJSON* event) { - if (!event || !g_db) { + if (!event || !db_is_available()) { return -1; } @@ -236,7 +235,7 @@ int store_config_event_in_database(const cJSON* event) { } cJSON* load_config_event_from_database(const char* relay_pubkey) { - if (!g_db || !relay_pubkey) { + if (!db_is_available() || !relay_pubkey) { return NULL; } @@ -481,7 +480,7 @@ int store_relay_private_key(const char* relay_privkey_hex) { } } - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for relay private key storage"); return -1; } @@ -495,7 +494,7 @@ int store_relay_private_key(const char* relay_privkey_hex) { } char* get_relay_private_key(void) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for relay private key retrieval"); return NULL; } @@ -1608,7 +1607,7 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s // Get value from config table const char* get_config_value_from_table(const char* key) { - if (!g_db || !key) { + if (!db_is_available() || !key) { return NULL; } @@ -1618,7 +1617,7 @@ const char* get_config_value_from_table(const char* key) { // Set value in config table int set_config_value_in_table(const char* key, const char* value, const char* data_type, const char* description, const char* category, int requires_restart) { - if (!g_db || !key || !value || !data_type) { + if (!db_is_available() || !key || !value || !data_type) { return -1; } @@ -1627,7 +1626,7 @@ int set_config_value_in_table(const char* key, const char* value, const char* da // Update config in table (simpler version of set_config_value_in_table) int update_config_in_table(const char* key, const char* value) { - if (!g_db || !key || !value) { + if (!db_is_available() || !key || !value) { return -1; } @@ -1647,21 +1646,15 @@ int update_config_in_table(const char* key, const char* value) { int populate_default_config_values(void) { DEBUG_TRACE("Entering populate_default_config_values()"); - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for populating default config values"); DEBUG_TRACE("Exiting populate_default_config_values() - no database"); return -1; } // Log config table row count at start of populate_default_config_values - sqlite3_stmt* count_stmt; - const char* count_sql = "SELECT COUNT(*) FROM config"; - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - // Row count check completed - } - sqlite3_finalize(count_stmt); - } + int config_row_count = 0; + (void)db_get_config_row_count(&config_row_count); DEBUG_LOG("Populating missing default configuration values in table..."); @@ -1674,22 +1667,12 @@ int populate_default_config_values(void) { const char* value = DEFAULT_CONFIG_VALUES[i].value; // Check if key already exists in config table - const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?"; - sqlite3_stmt* check_stmt; - - int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); - if (check_rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare config existence check"); - continue; - } - - sqlite3_bind_text(check_stmt, 1, key, -1, SQLITE_STATIC); - int key_exists = 0; - if (sqlite3_step(check_stmt) == SQLITE_ROW) { - key_exists = sqlite3_column_int(check_stmt, 0) > 0; + char* existing_value = db_get_config_value_dup(key); + if (existing_value) { + key_exists = 1; + free(existing_value); } - sqlite3_finalize(check_stmt); // Skip if key already exists (preserve existing configuration) if (key_exists) { @@ -1752,39 +1735,12 @@ int populate_default_config_values(void) { requires_restart = 1; } - // Only insert if key doesn't exist (INSERT will fail if key exists due to UNIQUE constraint) - const char* insert_sql = "INSERT INTO config (key, value, data_type, description, category, requires_restart) " - "VALUES (?, ?, ?, ?, ?, ?)"; - - sqlite3_stmt* insert_stmt; - int insert_rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &insert_stmt, NULL); - if (insert_rc != SQLITE_OK) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to prepare insert for: %s", key); - DEBUG_ERROR(error_msg); - continue; - } - - sqlite3_bind_text(insert_stmt, 1, key, -1, SQLITE_STATIC); - sqlite3_bind_text(insert_stmt, 2, value, -1, SQLITE_STATIC); - sqlite3_bind_text(insert_stmt, 3, data_type, -1, SQLITE_STATIC); - sqlite3_bind_text(insert_stmt, 4, "", -1, SQLITE_STATIC); - sqlite3_bind_text(insert_stmt, 5, category, -1, SQLITE_STATIC); - sqlite3_bind_int(insert_stmt, 6, requires_restart); - - int step_rc = sqlite3_step(insert_stmt); - sqlite3_finalize(insert_stmt); - - if (step_rc == SQLITE_DONE) { + if (db_set_config_value_full(key, value, data_type, "", category, requires_restart) == 0) { keys_added++; } else { - // Silently skip if key already exists (UNIQUE constraint violation) - if (step_rc != SQLITE_CONSTRAINT) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to insert default config: %s = %s (error: %s)", - key, value, sqlite3_errmsg(g_db)); - DEBUG_ERROR(error_msg); - } + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "Failed to insert default config: %s = %s", key, value); + DEBUG_ERROR(error_msg); } } @@ -1799,12 +1755,7 @@ int populate_default_config_values(void) { } // Log config table row count at end of populate_default_config_values - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - // Row count check completed - } - sqlite3_finalize(count_stmt); - } + (void)db_get_config_row_count(&config_row_count); DEBUG_TRACE("Exiting populate_default_config_values() - success"); return 0; @@ -1814,20 +1765,14 @@ int populate_default_config_values(void) { // The new populate_all_config_values_atomic() function handles pubkey storage atomically. // This function is kept for backward compatibility but should not be called in new code. int add_pubkeys_to_config_table(void) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for pubkey storage"); return -1; } // Log config table row count at start of add_pubkeys_to_config_table - sqlite3_stmt* count_stmt; - const char* count_sql = "SELECT COUNT(*) FROM config"; - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - // Row count check completed - } - sqlite3_finalize(count_stmt); - } + int config_row_count = 0; + (void)db_get_config_row_count(&config_row_count); DEBUG_INFO("Adding dynamically generated pubkeys to config table..."); @@ -1858,27 +1803,20 @@ int add_pubkeys_to_config_table(void) { } // If not in config table, try loading from old event-based config (migration scenario) - const char* sql = "SELECT pubkey FROM events WHERE kind = 33334 ORDER BY created_at DESC LIMIT 1"; - sqlite3_stmt* stmt; + char* event_pubkey = db_get_latest_event_pubkey_for_kind_dup(33334); + if (event_pubkey) { + if (strlen(event_pubkey) == 64) { + // Store in config table for future use + if (set_config_value_in_table("admin_pubkey", event_pubkey, "string", + "Administrator public key", "authentication", 0) == 0) { - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* event_pubkey = (const char*)sqlite3_column_text(stmt, 0); - if (event_pubkey && strlen(event_pubkey) == 64) { - // Store in config table for future use - if (set_config_value_in_table("admin_pubkey", event_pubkey, "string", - "Administrator public key", "authentication", 0) == 0) { - - // Admin pubkey is now stored directly in database, no cache update needed - - sqlite3_finalize(stmt); - DEBUG_INFO("✓ Migrated admin_pubkey from old config event to config table"); - return 0; - } + // Admin pubkey is now stored directly in database, no cache update needed + free(event_pubkey); + DEBUG_INFO("✓ Migrated admin_pubkey from old config event to config table"); + return 0; } } - sqlite3_finalize(stmt); + free(event_pubkey); } // Use cache value for storage (either from first-time startup or just loaded from DB) @@ -1914,12 +1852,7 @@ int add_pubkeys_to_config_table(void) { printf(" Relay pubkey: %s\n", relay_pubkey ? relay_pubkey : "NULL"); // Log config table row count at end of add_pubkeys_to_config_table - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - // Row count check completed - } - sqlite3_finalize(count_stmt); - } + (void)db_get_config_row_count(&config_row_count); return 0; } @@ -1994,8 +1927,7 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s } // Begin transaction for atomic config updates - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); - if (rc != SQLITE_OK) { + if (db_exec_sql("BEGIN IMMEDIATE TRANSACTION") != 0) { snprintf(error_message, error_size, "failed to begin config transaction"); return -1; } @@ -2032,13 +1964,13 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s } if (updates_applied > 0) { - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + (void)db_exec_sql("COMMIT"); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates", updates_applied); DEBUG_INFO(success_msg); } else { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + (void)db_exec_sql("ROLLBACK"); snprintf(error_message, error_size, "no valid configuration parameters found"); return -1; } @@ -2077,7 +2009,7 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz // Add auth rule from configuration int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value) { - if (!g_db || !rule_type || !pattern_type || !pattern_value) { + if (!db_is_available() || !rule_type || !pattern_type || !pattern_value) { return -1; } @@ -2087,7 +2019,7 @@ int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, // Remove auth rule from configuration int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value) { - if (!g_db || !rule_type || !pattern_type || !pattern_value) { + if (!db_is_available() || !rule_type || !pattern_type || !pattern_value) { return -1; } @@ -2106,7 +2038,7 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type * @return 0 on success, -1 on error */ int wot_sync_from_admin_kind3(void) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("WoT sync: database not available"); return -1; } @@ -2146,16 +2078,16 @@ int wot_sync_from_admin_kind3(void) { "WHERE e.kind = 3 AND e.pubkey = ? AND et.tag_name = 'p' " "AND e.created_at = (SELECT MAX(created_at) FROM events WHERE kind = 3 AND pubkey = ?)"; - rc = sqlite3_prepare_v2(g_db, query_sql, -1, &stmt, NULL); + rc = db_prepare(query_sql, &stmt); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to prepare p-tag query: %s", sqlite3_errmsg(g_db)); + DEBUG_ERROR("WoT sync: Failed to prepare p-tag query: %s", db_last_error()); free((char*)admin_pubkey); free((char*)relay_pubkey); return -1; } - sqlite3_bind_text(stmt, 1, admin_pubkey, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, admin_pubkey, -1, SQLITE_STATIC); + db_bind_text_param(stmt, 1, admin_pubkey); + db_bind_text_param(stmt, 2, admin_pubkey); // Collect all p tags char** p_tags = NULL; @@ -2165,14 +2097,14 @@ int wot_sync_from_admin_kind3(void) { p_tags = malloc(p_tag_capacity * sizeof(char*)); if (!p_tags) { DEBUG_ERROR("WoT sync: Failed to allocate p_tag array"); - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); free((char*)admin_pubkey); free((char*)relay_pubkey); return -1; } - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { - const char* p_tag = (const char*)sqlite3_column_text(stmt, 0); + while ((rc = db_step_stmt(stmt)) == SQLITE_ROW) { + const char* p_tag = (const char*)db_column_text_value(stmt, 0); if (p_tag) { // Expand array if needed if (p_tag_count >= p_tag_capacity) { @@ -2185,7 +2117,7 @@ int wot_sync_from_admin_kind3(void) { free(p_tags[i]); } free(p_tags); - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); free((char*)admin_pubkey); free((char*)relay_pubkey); return -1; @@ -2196,7 +2128,7 @@ int wot_sync_from_admin_kind3(void) { p_tag_count++; } } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); // If no kind 3 event found, just log and continue (admin and relay will still be whitelisted) if (p_tag_count == 0) { @@ -2206,9 +2138,9 @@ int wot_sync_from_admin_kind3(void) { } // Step 2: Begin transaction - rc = sqlite3_exec(g_db, "BEGIN TRANSACTION", NULL, NULL, NULL); + rc = db_exec_sql("BEGIN TRANSACTION"); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to begin transaction: %s", sqlite3_errmsg(g_db)); + DEBUG_ERROR("WoT sync: Failed to begin transaction: %s", db_last_error()); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2219,11 +2151,9 @@ int wot_sync_from_admin_kind3(void) { } // Step 3: Clear existing wot_whitelist rules - const char* delete_sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'"; - rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to clear existing wot_whitelist rules: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + if (db_delete_wot_whitelist_rules() != 0) { + DEBUG_ERROR("WoT sync: Failed to clear existing wot_whitelist rules: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2239,10 +2169,10 @@ int wot_sync_from_admin_kind3(void) { "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, active) " "VALUES ('wot_whitelist', 'pubkey', ?, 1)"; - rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL); + rc = db_prepare(insert_sql, &stmt); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to prepare insert statement: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to prepare insert statement: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2252,13 +2182,13 @@ int wot_sync_from_admin_kind3(void) { return -1; } - sqlite3_bind_text(stmt, 1, admin_pubkey, -1, SQLITE_STATIC); - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); + db_bind_text_param(stmt, 1, admin_pubkey); + rc = db_step_stmt(stmt); + db_finalize_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("WoT sync: Failed to insert admin pubkey whitelist: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to insert admin pubkey whitelist: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2271,10 +2201,10 @@ int wot_sync_from_admin_kind3(void) { DEBUG_TRACE("WoT sync: Whitelisted admin pubkey: %.16s...", admin_pubkey); // Step 5: Insert relay pubkey as wot_whitelist (needed for DM responses) - rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL); + rc = db_prepare(insert_sql, &stmt); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to prepare insert statement for relay: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to prepare insert statement for relay: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2284,13 +2214,13 @@ int wot_sync_from_admin_kind3(void) { return -1; } - sqlite3_bind_text(stmt, 1, relay_pubkey, -1, SQLITE_STATIC); - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); + db_bind_text_param(stmt, 1, relay_pubkey); + rc = db_step_stmt(stmt); + db_finalize_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("WoT sync: Failed to insert relay pubkey whitelist: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to insert relay pubkey whitelist: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -2304,10 +2234,10 @@ int wot_sync_from_admin_kind3(void) { // Step 6: Insert each p tag from kind 3 event for (int i = 0; i < p_tag_count; i++) { - rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL); + rc = db_prepare(insert_sql, &stmt); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to prepare insert for p_tag %d: %s", i, sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to prepare insert for p_tag %d: %s", i, db_last_error()); + db_exec_sql("ROLLBACK"); // Free remaining tags for (int j = i; j < p_tag_count; j++) { free(p_tags[j]); @@ -2318,13 +2248,13 @@ int wot_sync_from_admin_kind3(void) { return -1; } - sqlite3_bind_text(stmt, 1, p_tags[i], -1, SQLITE_STATIC); - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); + db_bind_text_param(stmt, 1, p_tags[i]); + rc = db_step_stmt(stmt); + db_finalize_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("WoT sync: Failed to insert p_tag %d: %s", i, sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to insert p_tag %d: %s", i, db_last_error()); + db_exec_sql("ROLLBACK"); // Free remaining tags for (int j = i; j < p_tag_count; j++) { free(p_tags[j]); @@ -2338,10 +2268,10 @@ int wot_sync_from_admin_kind3(void) { } // Step 7: Commit transaction - rc = sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + rc = db_exec_sql("COMMIT"); if (rc != SQLITE_OK) { - DEBUG_ERROR("WoT sync: Failed to commit transaction: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + DEBUG_ERROR("WoT sync: Failed to commit transaction: %s", db_last_error()); + db_exec_sql("ROLLBACK"); for (int i = 0; i < p_tag_count; i++) { free(p_tags[i]); } @@ -3008,7 +2938,7 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3044,20 +2974,20 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ // Execute query sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + int rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare auth query"); return -1; } if (use_pattern_param && pattern_value) { - sqlite3_bind_text(stmt, 1, pattern_value, -1, SQLITE_STATIC); + db_bind_text_param(stmt, 1, pattern_value); } // Build results array cJSON* results_array = cJSON_CreateArray(); if (!results_array) { - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); snprintf(error_message, error_size, "failed to create results array"); return -1; } @@ -3065,10 +2995,10 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ int rule_count = 0; // printf("=== Auth Query Results (%s) ===\n", query_type); - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); - const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); - const char* pattern_value_result = (const char*)sqlite3_column_text(stmt, 2); + while (db_step_stmt(stmt) == SQLITE_ROW) { + const char* rule_type = (const char*)db_column_text_value(stmt, 0); + const char* pattern_type = (const char*)db_column_text_value(stmt, 1); + const char* pattern_value_result = (const char*)db_column_text_value(stmt, 2); // printf(" %s %s:%s -> %s\n", // rule_type ? rule_type : "", @@ -3087,7 +3017,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ rule_count++; } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); // Build and send response with mapped query type for frontend routing const char* mapped_query_type = map_auth_query_type_to_response(query_type); @@ -3122,7 +3052,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3162,32 +3092,32 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro // Execute query sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + int rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare config query"); return -1; } if (use_pattern_param && pattern_value) { - sqlite3_bind_text(stmt, 1, pattern_value, -1, SQLITE_STATIC); + db_bind_text_param(stmt, 1, pattern_value); } // Build results array cJSON* results_array = cJSON_CreateArray(); if (!results_array) { - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); snprintf(error_message, error_size, "failed to create results array"); return -1; } int config_count = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* key = (const char*)sqlite3_column_text(stmt, 0); - const char* value = (const char*)sqlite3_column_text(stmt, 1); - const char* data_type = (const char*)sqlite3_column_text(stmt, 2); - const char* category = (const char*)sqlite3_column_text(stmt, 3); - const char* description = (const char*)sqlite3_column_text(stmt, 4); + while (db_step_stmt(stmt) == SQLITE_ROW) { + const char* key = (const char*)db_column_text_value(stmt, 0); + const char* value = (const char*)db_column_text_value(stmt, 1); + const char* data_type = (const char*)db_column_text_value(stmt, 2); + const char* category = (const char*)db_column_text_value(stmt, 3); + const char* description = (const char*)db_column_text_value(stmt, 4); // Add config item to results array @@ -3202,7 +3132,7 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro config_count++; } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); // Build and send response with mapped query type for frontend routing const char* mapped_query_type = map_config_query_type_to_response(query_type); @@ -3237,7 +3167,7 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3252,22 +3182,13 @@ int handle_config_set_unified(cJSON* event, const char* config_key, const char* } // Check if the config key exists in the table - const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?"; - sqlite3_stmt* check_stmt; - - int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); - if (check_rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare config existence check"); + int config_exists_count = 0; + const char* config_exists_params[] = { config_key }; + if (db_count_with_sql("SELECT COUNT(*) FROM config WHERE key = ?", config_exists_params, 1, &config_exists_count) != 0) { + snprintf(error_message, error_size, "failed to check config existence"); return -1; } - - sqlite3_bind_text(check_stmt, 1, config_key, -1, SQLITE_STATIC); - - int config_exists = 0; - if (sqlite3_step(check_stmt) == SQLITE_ROW) { - config_exists = sqlite3_column_int(check_stmt, 0) > 0; - } - sqlite3_finalize(check_stmt); + int config_exists = config_exists_count > 0; if (!config_exists) { snprintf(error_message, error_size, "error: configuration key '%s' not found", config_key); @@ -3289,7 +3210,7 @@ int handle_config_set_unified(cJSON* event, const char* config_key, const char* } else { // Level 0: clear wot_whitelist rules and reset auth flags DEBUG_INFO("Config set: wot_enabled changed to 0, clearing wot_whitelist rules"); - sqlite3_exec(g_db, "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'", NULL, NULL, NULL); + (void)db_delete_wot_whitelist_rules(); update_config_in_table("auth_enabled", "false"); update_config_in_table("nip42_auth_required_subscriptions", "false"); } @@ -3329,33 +3250,21 @@ int handle_config_set_unified(cJSON* event, const char* config_key, const char* int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } if (strcmp(command, "clear_all_auth_rules") == 0) { // Count existing rules first - const char* count_sql = "SELECT COUNT(*) FROM auth_rules"; - sqlite3_stmt* count_stmt; - - int rc = sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL); - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare count query"); + int rule_count = 0; + if (db_count_with_sql("SELECT COUNT(*) FROM auth_rules", NULL, 0, &rule_count) != 0) { + snprintf(error_message, error_size, "failed to count auth rules"); return -1; } - - int rule_count = 0; - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - rule_count = sqlite3_column_int(count_stmt, 0); - } - sqlite3_finalize(count_stmt); - + // Delete all auth rules - const char* delete_sql = "DELETE FROM auth_rules"; - rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); - - if (rc != SQLITE_OK) { + if (db_exec_sql("DELETE FROM auth_rules") != 0) { snprintf(error_message, error_size, "failed to execute clear auth rules command"); return -1; } @@ -3405,24 +3314,14 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error printf(" Pattern value: %s\n", pattern_value); // Check if rule exists before deletion - const char* check_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; - sqlite3_stmt* check_stmt; - - int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); - if (check_rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare rule existence check"); + int rule_exists_count = 0; + const char* rule_exists_params[] = { rule_type, pattern_type, pattern_value }; + if (db_count_with_sql("SELECT COUNT(*) FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?", + rule_exists_params, 3, &rule_exists_count) != 0) { + snprintf(error_message, error_size, "failed to check rule existence"); return -1; } - - sqlite3_bind_text(check_stmt, 1, rule_type, -1, SQLITE_STATIC); - sqlite3_bind_text(check_stmt, 2, pattern_type, -1, SQLITE_STATIC); - sqlite3_bind_text(check_stmt, 3, pattern_value, -1, SQLITE_STATIC); - - int rule_exists = 0; - if (sqlite3_step(check_stmt) == SQLITE_ROW) { - rule_exists = sqlite3_column_int(check_stmt, 0) > 0; - } - sqlite3_finalize(check_stmt); + int rule_exists = rule_exists_count > 0; if (!rule_exists) { snprintf(error_message, error_size, "error: auth rule not found"); @@ -3472,7 +3371,7 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON* status_data = cJSON_CreateObject(); - cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available"); + cJSON_AddStringToObject(status_data, "database", db_is_available() ? "connected" : "not_available"); cJSON_AddStringToObject(status_data, "cache_status", "not_used"); if (strlen(g_database_path) > 0) { @@ -3480,30 +3379,22 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error } // Count configuration items and auth rules - if (g_db) { - sqlite3_stmt* stmt; - - // Config count - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON_AddNumberToObject(status_data, "config_items", sqlite3_column_int(stmt, 0)); - } - sqlite3_finalize(stmt); + if (db_is_available()) { + int config_items = 0; + if (db_get_config_row_count(&config_items) == 0) { + cJSON_AddNumberToObject(status_data, "config_items", config_items); } - // Auth rules count - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON_AddNumberToObject(status_data, "auth_rules", sqlite3_column_int(stmt, 0)); - } - sqlite3_finalize(stmt); + int auth_rules = 0; + if (db_count_with_sql("SELECT COUNT(*) FROM auth_rules", NULL, 0, &auth_rules) == 0) { + cJSON_AddNumberToObject(status_data, "auth_rules", auth_rules); } } cJSON_AddItemToObject(response, "data", status_data); printf("=== System Status ===\n"); - printf("Database: %s\n", g_db ? "Connected" : "Not available"); + printf("Database: %s\n", db_is_available() ? "Connected" : "Not available"); printf("Cache status: Not used (direct database queries)\n"); // Get admin pubkey from event for response @@ -3575,30 +3466,15 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error int wot_enabled = get_config_int("wot_enabled", 0); // Check if admin's kind 3 event exists - int admin_kind3_exists = 0; - const char* kind3_sql = "SELECT COUNT(*) FROM events WHERE kind = 3 AND pubkey = ?"; - sqlite3_stmt* kind3_stmt; - - int rc = sqlite3_prepare_v2(g_db, kind3_sql, -1, &kind3_stmt, NULL); - if (rc == SQLITE_OK) { - sqlite3_bind_text(kind3_stmt, 1, admin_pubkey, -1, SQLITE_STATIC); - if (sqlite3_step(kind3_stmt) == SQLITE_ROW) { - admin_kind3_exists = sqlite3_column_int(kind3_stmt, 0) > 0; - } - sqlite3_finalize(kind3_stmt); + int admin_kind3_count = 0; + const char* kind3_params[] = { admin_pubkey }; + if (db_count_with_sql("SELECT COUNT(*) FROM events WHERE kind = 3 AND pubkey = ?", kind3_params, 1, &admin_kind3_count) != 0) { + admin_kind3_count = 0; } - + int admin_kind3_exists = admin_kind3_count > 0; + // Count WoT whitelist entries - int wot_whitelist_count = 0; - const char* count_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; - sqlite3_stmt* count_stmt; - - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - wot_whitelist_count = sqlite3_column_int(count_stmt, 0); - } - sqlite3_finalize(count_stmt); - } + int wot_whitelist_count = db_count_wot_whitelist_rules(); // Build response cJSON* response = cJSON_CreateObject(); @@ -3640,16 +3516,7 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error int sync_result = wot_sync_from_admin_kind3(); // Get updated whitelist count - int wot_whitelist_count = 0; - const char* count_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; - sqlite3_stmt* count_stmt; - - if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - wot_whitelist_count = sqlite3_column_int(count_stmt, 0); - } - sqlite3_finalize(count_stmt); - } + int wot_whitelist_count = db_count_wot_whitelist_rules(); // Build response cJSON* response = cJSON_CreateObject(); @@ -3690,8 +3557,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz } // Begin transaction for atomic auth rule updates - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); - if (rc != SQLITE_OK) { + if (db_exec_sql("BEGIN IMMEDIATE TRANSACTION") != 0) { snprintf(error_message, error_size, "failed to begin auth rule transaction"); return -1; } @@ -3699,7 +3565,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz int rules_processed = 0; cJSON* processed_rules = cJSON_CreateArray(); if (!processed_rules) { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); snprintf(error_message, error_size, "failed to create response array"); return -1; } @@ -3749,7 +3615,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz } if (rules_processed > 0) { - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + db_exec_sql("COMMIT"); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed); @@ -3785,7 +3651,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz snprintf(error_message, error_size, "failed to send auth rule modification response"); return -1; } else { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); cJSON_Delete(processed_rules); snprintf(error_message, error_size, "no valid auth rules found"); return -1; @@ -3795,7 +3661,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3815,33 +3681,33 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s // Query total events count sqlite3_stmt* stmt; - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0)); + if (db_prepare("SELECT COUNT(*) FROM events", &stmt) == SQLITE_OK) { + if (db_step_stmt(stmt) == SQLITE_ROW) { + cJSON_AddNumberToObject(response, "total_events", db_column_int64_value(stmt, 0)); } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } // Query event kinds distribution cJSON* event_kinds = cJSON_CreateArray(); - if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { + if (db_prepare("SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", &stmt) == SQLITE_OK) { + while (db_step_stmt(stmt) == SQLITE_ROW) { cJSON* kind_obj = cJSON_CreateObject(); - cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0)); - cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1)); - cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2)); + cJSON_AddNumberToObject(kind_obj, "kind", db_column_int_value(stmt, 0)); + cJSON_AddNumberToObject(kind_obj, "count", db_column_int64_value(stmt, 1)); + cJSON_AddNumberToObject(kind_obj, "percentage", db_column_double_value(stmt, 2)); cJSON_AddItemToArray(event_kinds, kind_obj); } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } cJSON_AddItemToObject(response, "event_kinds", event_kinds); // Query time-based statistics cJSON* time_stats = cJSON_CreateObject(); - if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* period = (const char*)sqlite3_column_text(stmt, 0); - sqlite3_int64 count = sqlite3_column_int64(stmt, 1); + if (db_prepare("SELECT period, total_events FROM time_stats_view", &stmt) == SQLITE_OK) { + while (db_step_stmt(stmt) == SQLITE_ROW) { + const char* period = (const char*)db_column_text_value(stmt, 0); + sqlite3_int64 count = db_column_int64_value(stmt, 1); if (strcmp(period, "total") == 0) { cJSON_AddNumberToObject(time_stats, "total", count); @@ -3853,45 +3719,45 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s cJSON_AddNumberToObject(time_stats, "last_30d", count); } } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } cJSON_AddItemToObject(response, "time_stats", time_stats); // Query top pubkeys cJSON* top_pubkeys = cJSON_CreateArray(); - if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { + if (db_prepare("SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", &stmt) == SQLITE_OK) { + while (db_step_stmt(stmt) == SQLITE_ROW) { cJSON* pubkey_obj = cJSON_CreateObject(); - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); + const char* pubkey = (const char*)db_column_text_value(stmt, 0); cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : ""); - cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1)); - cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2)); + cJSON_AddNumberToObject(pubkey_obj, "event_count", db_column_int64_value(stmt, 1)); + cJSON_AddNumberToObject(pubkey_obj, "percentage", db_column_double_value(stmt, 2)); cJSON_AddItemToArray(top_pubkeys, pubkey_obj); } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys); // Get database creation timestamp (oldest event) - if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0); + if (db_prepare("SELECT MIN(created_at) FROM events", &stmt) == SQLITE_OK) { + if (db_step_stmt(stmt) == SQLITE_ROW) { + sqlite3_int64 oldest_timestamp = db_column_int64_value(stmt, 0); if (oldest_timestamp > 0) { cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp); } } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } // Get latest event timestamp - if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0); + if (db_prepare("SELECT MAX(created_at) FROM events", &stmt) == SQLITE_OK) { + if (db_step_stmt(stmt) == SQLITE_ROW) { + sqlite3_int64 latest_timestamp = db_column_int64_value(stmt, 0); if (latest_timestamp > 0) { cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp); } } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); } @@ -3955,7 +3821,7 @@ int handle_create_relay_event_unified(cJSON* event, const char* kind_str, const int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!g_db) { + if (!db_is_available()) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -4017,7 +3883,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error } // Begin transaction for atomic config updates - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION"); if (rc != SQLITE_OK) { cJSON_Delete(config_objects_array); snprintf(error_message, error_size, "failed to begin config update transaction"); @@ -4030,7 +3896,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error char first_error_field[128] = {0}; // Track which field failed first cJSON* processed_configs = cJSON_CreateArray(); if (!processed_configs) { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); cJSON_Delete(config_objects_array); snprintf(error_message, error_size, "failed to create response array"); return -1; @@ -4096,20 +3962,20 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?"; sqlite3_stmt* check_stmt; - int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); + int check_rc = db_prepare(check_sql, &check_stmt); if (check_rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare config existence check"); validation_errors++; continue; } - sqlite3_bind_text(check_stmt, 1, key, -1, SQLITE_STATIC); + db_bind_text_param(check_stmt, 1, key); int config_exists = 0; - if (sqlite3_step(check_stmt) == SQLITE_ROW) { - config_exists = sqlite3_column_int(check_stmt, 0) > 0; + if (db_step_stmt(check_stmt) == SQLITE_ROW) { + config_exists = db_column_int_value(check_stmt, 0) > 0; } - sqlite3_finalize(check_stmt); + db_finalize_stmt(check_stmt); if (!config_exists) { DEBUG_ERROR("Configuration key not found"); @@ -4133,12 +3999,12 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error sqlite3_stmt* restart_stmt; int requires_restart = 0; - if (sqlite3_prepare_v2(g_db, requires_restart_sql, -1, &restart_stmt, NULL) == SQLITE_OK) { - sqlite3_bind_text(restart_stmt, 1, key, -1, SQLITE_STATIC); - if (sqlite3_step(restart_stmt) == SQLITE_ROW) { - requires_restart = sqlite3_column_int(restart_stmt, 0); + if (db_prepare(requires_restart_sql, &restart_stmt) == SQLITE_OK) { + db_bind_text_param(restart_stmt, 1, key); + if (db_step_stmt(restart_stmt) == SQLITE_ROW) { + requires_restart = db_column_int_value(restart_stmt, 0); } - sqlite3_finalize(restart_stmt); + db_finalize_stmt(restart_stmt); } // Update the configuration value in the table @@ -4212,14 +4078,14 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error // Determine transaction outcome if (updates_applied > 0 && validation_errors == 0) { // All updates successful - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + db_exec_sql("COMMIT"); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates successfully", updates_applied); DEBUG_INFO(success_msg); } else if (updates_applied > 0 && validation_errors > 0) { // Partial success - rollback for atomic behavior - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Config update failed: %d validation errors (atomic rollback)", validation_errors); @@ -4262,7 +4128,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error return -1; } else { // No updates applied - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); // Build error response for no valid updates cJSON* error_response = cJSON_CreateObject(); @@ -4332,7 +4198,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error // Apply CLI overrides to existing config table in a single atomic operation int apply_cli_overrides_atomic(const cli_options_t* cli_options) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for CLI overrides"); return -1; } @@ -4356,11 +4222,9 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) { DEBUG_INFO("Applying CLI overrides atomically"); // Begin transaction - char* err_msg = NULL; - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, &err_msg); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to begin CLI overrides transaction: %s", err_msg); - sqlite3_free(err_msg); + int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION"); + if (rc != 0) { + DEBUG_ERROR("Failed to begin CLI overrides transaction: %s", db_last_error()); return -1; } @@ -4371,7 +4235,7 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) { if (update_config_in_table("relay_port", port_str) != 0) { DEBUG_ERROR("Failed to update relay_port override"); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); return -1; } DEBUG_INFO("Applied CLI override: relay_port = %s", port_str); @@ -4381,7 +4245,7 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) { if (cli_options->admin_pubkey_override[0] != '\0') { if (update_config_in_table("admin_pubkey", cli_options->admin_pubkey_override) != 0) { DEBUG_ERROR("Failed to update admin_pubkey override"); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); return -1; } DEBUG_INFO("Applied CLI override: admin_pubkey"); @@ -4391,18 +4255,17 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) { if (cli_options->relay_privkey_override[0] != '\0') { if (update_config_in_table("relay_privkey", cli_options->relay_privkey_override) != 0) { DEBUG_ERROR("Failed to update relay_privkey override"); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); return -1; } DEBUG_INFO("Applied CLI override: relay_privkey"); } // Commit transaction - rc = sqlite3_exec(g_db, "COMMIT", NULL, NULL, &err_msg); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to commit CLI overrides transaction: %s", err_msg); - sqlite3_free(err_msg); - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + rc = db_exec_sql("COMMIT"); + if (rc != 0) { + DEBUG_ERROR("Failed to commit CLI overrides transaction: %s", db_last_error()); + db_exec_sql("ROLLBACK"); return -1; } @@ -4414,7 +4277,7 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) { // Populate all config values atomically in a single transaction int populate_all_config_values_atomic(const char* admin_pubkey, const char* relay_pubkey) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not initialized"); return -1; } @@ -4425,21 +4288,19 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela } // Begin transaction - char* err_msg = NULL; - int rc = sqlite3_exec(g_db, "BEGIN TRANSACTION;", NULL, NULL, &err_msg); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to begin transaction: %s", err_msg); - sqlite3_free(err_msg); + int rc = db_exec_sql("BEGIN TRANSACTION;"); + if (rc != 0) { + DEBUG_ERROR("Failed to begin transaction: %s", db_last_error()); return -1; } // Prepare INSERT OR REPLACE statement with all required fields sqlite3_stmt* stmt = NULL; const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) VALUES (?, ?, ?, ?, ?, ?)"; - rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_db)); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + DEBUG_ERROR("Failed to prepare statement: %s", db_last_error()); + db_exec_sql("ROLLBACK;"); return -1; } @@ -4508,81 +4369,80 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela requires_restart = 1; } - sqlite3_reset(stmt); - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, "", -1, SQLITE_STATIC); // description (empty for defaults) - sqlite3_bind_text(stmt, 5, category, -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 6, requires_restart); + db_reset_stmt(stmt); + db_bind_text_param(stmt, 1, key); + db_bind_text_param(stmt, 2, value); + db_bind_text_param(stmt, 3, data_type); + db_bind_text_param(stmt, 4, ""); // description (empty for defaults) + db_bind_text_param(stmt, 5, category); + db_bind_int_param(stmt, 6, requires_restart); - rc = sqlite3_step(stmt); + rc = db_step_stmt(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert config key '%s': %s", - key, sqlite3_errmsg(g_db)); - sqlite3_finalize(stmt); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + key, db_last_error()); + db_finalize_stmt(stmt); + db_exec_sql("ROLLBACK;"); return -1; } } // Insert admin_pubkey - sqlite3_reset(stmt); - sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, admin_pubkey, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, "string", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, "Administrator public key", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 5, "authentication", -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 6, 0); // does not require restart - rc = sqlite3_step(stmt); + db_reset_stmt(stmt); + db_bind_text_param(stmt, 1, "admin_pubkey"); + db_bind_text_param(stmt, 2, admin_pubkey); + db_bind_text_param(stmt, 3, "string"); + db_bind_text_param(stmt, 4, "Administrator public key"); + db_bind_text_param(stmt, 5, "authentication"); + db_bind_int_param(stmt, 6, 0); // does not require restart + rc = db_step_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("Failed to insert admin_pubkey: %s", sqlite3_errmsg(g_db)); - sqlite3_finalize(stmt); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + DEBUG_ERROR("Failed to insert admin_pubkey: %s", db_last_error()); + db_finalize_stmt(stmt); + db_exec_sql("ROLLBACK;"); return -1; } // Insert relay_pubkey - sqlite3_reset(stmt); - sqlite3_bind_text(stmt, 1, "relay_pubkey", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, relay_pubkey, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, "string", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, "Relay public key", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 5, "relay", -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 6, 0); // does not require restart - rc = sqlite3_step(stmt); + db_reset_stmt(stmt); + db_bind_text_param(stmt, 1, "relay_pubkey"); + db_bind_text_param(stmt, 2, relay_pubkey); + db_bind_text_param(stmt, 3, "string"); + db_bind_text_param(stmt, 4, "Relay public key"); + db_bind_text_param(stmt, 5, "relay"); + db_bind_int_param(stmt, 6, 0); // does not require restart + rc = db_step_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("Failed to insert relay_pubkey: %s", sqlite3_errmsg(g_db)); - sqlite3_finalize(stmt); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + DEBUG_ERROR("Failed to insert relay_pubkey: %s", db_last_error()); + db_finalize_stmt(stmt); + db_exec_sql("ROLLBACK;"); return -1; } // Insert monitoring system config entry (ephemeral kind 24567) // Note: Monitoring is automatically activated when clients subscribe to kind 24567 - sqlite3_reset(stmt); - sqlite3_bind_text(stmt, 1, "kind_24567_reporting_throttle_sec", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, "5", -1, SQLITE_STATIC); // integer, default 5 seconds - sqlite3_bind_text(stmt, 3, "integer", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, "Minimum seconds between monitoring event reports (ephemeral kind 24567)", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 5, "monitoring", -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 6, 0); // does not require restart - rc = sqlite3_step(stmt); + db_reset_stmt(stmt); + db_bind_text_param(stmt, 1, "kind_24567_reporting_throttle_sec"); + db_bind_text_param(stmt, 2, "5"); // integer, default 5 seconds + db_bind_text_param(stmt, 3, "integer"); + db_bind_text_param(stmt, 4, "Minimum seconds between monitoring event reports (ephemeral kind 24567)"); + db_bind_text_param(stmt, 5, "monitoring"); + db_bind_int_param(stmt, 6, 0); // does not require restart + rc = db_step_stmt(stmt); if (rc != SQLITE_DONE) { - DEBUG_ERROR("Failed to insert kind_24567_reporting_throttle_sec: %s", sqlite3_errmsg(g_db)); - sqlite3_finalize(stmt); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + DEBUG_ERROR("Failed to insert kind_24567_reporting_throttle_sec: %s", db_last_error()); + db_finalize_stmt(stmt); + db_exec_sql("ROLLBACK;"); return -1; } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); // Commit transaction - rc = sqlite3_exec(g_db, "COMMIT;", NULL, NULL, &err_msg); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to commit transaction: %s", err_msg); - sqlite3_free(err_msg); - sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL); + rc = db_exec_sql("COMMIT;"); + if (rc != 0) { + DEBUG_ERROR("Failed to commit transaction: %s", db_last_error()); + db_exec_sql("ROLLBACK;"); return -1; } @@ -4620,21 +4480,21 @@ const char* get_config_value_hybrid(const char* key) { // Check if config table is ready int is_config_table_ready(void) { - if (!g_db) return 0; + if (!db_is_available()) return 0; const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'"; sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + int rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { return 0; } int table_exists = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - table_exists = sqlite3_column_int(stmt, 0) > 0; + if (db_step_stmt(stmt) == SQLITE_ROW) { + table_exists = db_column_int_value(stmt, 0) > 0; } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); if (!table_exists) { return 0; @@ -4642,16 +4502,16 @@ int is_config_table_ready(void) { // Check if table has configuration data const char* count_sql = "SELECT COUNT(*) FROM config"; - rc = sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL); + rc = db_prepare(count_sql, &stmt); if (rc != SQLITE_OK) { return 0; } int config_count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - config_count = sqlite3_column_int(stmt, 0); + if (db_step_stmt(stmt) == SQLITE_ROW) { + config_count = db_column_int_value(stmt, 0); } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); return config_count > 0; } @@ -4702,7 +4562,7 @@ int retry_store_initial_config_event(void) { // Populate config table from a configuration event int populate_config_table_from_event(const cJSON* event) { - if (!event || !g_db) { + if (!event || !db_is_available()) { return -1; } @@ -4814,7 +4674,7 @@ int populate_config_table_from_event(const cJSON* event) { // Migrate configuration from existing events to config table int migrate_config_from_events_to_table(void) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for configuration migration"); return -1; } @@ -4850,7 +4710,7 @@ int migrate_config_from_events_to_table(void) { // Process startup configuration event - bypasses auth and updates config table int process_startup_config_event(const cJSON* event) { - if (!event || !g_db) { + if (!event || !db_is_available()) { DEBUG_ERROR("Invalid parameters for startup config processing"); return -1; } @@ -4870,7 +4730,7 @@ int process_startup_config_event(const cJSON* event) { } // Begin transaction for atomic config updates - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION"); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to begin startup config transaction"); return -1; @@ -4907,7 +4767,7 @@ int process_startup_config_event(const cJSON* event) { } if (updates_applied > 0) { - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + db_exec_sql("COMMIT"); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), @@ -4915,7 +4775,7 @@ int process_startup_config_event(const cJSON* event) { DEBUG_INFO(success_msg); return 0; } else { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + db_exec_sql("ROLLBACK"); DEBUG_ERROR("No valid configuration parameters found in startup event"); return -1; } @@ -4958,7 +4818,7 @@ int process_startup_config_event_with_fallback(const cJSON* event) { // Generate synthetic configuration event from current config table data cJSON* generate_config_event_from_table(void) { - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for config event generation"); return NULL; } @@ -5009,7 +4869,7 @@ cJSON* generate_config_event_from_table(void) { const char* sql = "SELECT key, value FROM config ORDER BY key"; sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + int rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare config query for event generation"); cJSON_Delete(tags); @@ -5020,9 +4880,9 @@ cJSON* generate_config_event_from_table(void) { int config_items_added = 0; // Add each config item as a tag - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* key = (const char*)sqlite3_column_text(stmt, 0); - const char* value = (const char*)sqlite3_column_text(stmt, 1); + while (db_step_stmt(stmt) == SQLITE_ROW) { + const char* key = (const char*)db_column_text_value(stmt, 0); + const char* value = (const char*)db_column_text_value(stmt, 1); if (key && value) { cJSON* config_tag = cJSON_CreateArray(); @@ -5033,7 +4893,7 @@ cJSON* generate_config_event_from_table(void) { } } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); if (config_items_added == 0) { DEBUG_WARN("No configuration items found in config table for event generation"); diff --git a/src/db_ops.c b/src/db_ops.c index bc138e2..c520108 100644 --- a/src/db_ops.c +++ b/src/db_ops.c @@ -20,6 +20,69 @@ sqlite3* db_get_handle(void) { return g_db; } +const char* db_last_error(void) { + if (!g_db) return "database not available"; + return sqlite3_errmsg(g_db); +} + +const char* db_get_database_path(void) { + return g_database_path; +} + +int db_prepare(const char* sql, sqlite3_stmt** out_stmt) { + if (!g_db || !sql || !out_stmt) return SQLITE_MISUSE; + return sqlite3_prepare_v2(g_db, sql, -1, out_stmt, NULL); +} + +int db_bind_text_param(sqlite3_stmt* stmt, int index, const char* value) { + if (!stmt) return SQLITE_MISUSE; + return sqlite3_bind_text(stmt, index, value ? value : "", -1, SQLITE_TRANSIENT); +} + +int db_bind_int_param(sqlite3_stmt* stmt, int index, int value) { + if (!stmt) return SQLITE_MISUSE; + return sqlite3_bind_int(stmt, index, value); +} + +int db_bind_int64_param(sqlite3_stmt* stmt, int index, long long value) { + if (!stmt) return SQLITE_MISUSE; + return sqlite3_bind_int64(stmt, index, (sqlite3_int64)value); +} + +int db_step_stmt(sqlite3_stmt* stmt) { + if (!stmt) return SQLITE_MISUSE; + return sqlite3_step(stmt); +} + +int db_reset_stmt(sqlite3_stmt* stmt) { + if (!stmt) return SQLITE_MISUSE; + return sqlite3_reset(stmt); +} + +const char* db_column_text_value(sqlite3_stmt* stmt, int col) { + if (!stmt) return NULL; + return (const char*)sqlite3_column_text(stmt, col); +} + +int db_column_int_value(sqlite3_stmt* stmt, int col) { + if (!stmt) return 0; + return sqlite3_column_int(stmt, col); +} + +long long db_column_int64_value(sqlite3_stmt* stmt, int col) { + if (!stmt) return 0; + return (long long)sqlite3_column_int64(stmt, col); +} + +double db_column_double_value(sqlite3_stmt* stmt, int col) { + if (!stmt) return 0.0; + return sqlite3_column_double(stmt, col); +} + +void db_finalize_stmt(sqlite3_stmt* stmt) { + if (stmt) sqlite3_finalize(stmt); +} + int db_log_subscription_created(const char* sub_id, const char* wsi_ptr, const char* client_ip, const char* filter_json) { if (!g_db || !sub_id || !wsi_ptr || !client_ip) return -1; @@ -315,6 +378,155 @@ int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_ return 0; } +char* 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) { + if (!g_db || !query || !request_id || !error_message) return NULL; + + sqlite3_busy_timeout(g_db, timeout_ms > 0 ? timeout_ms : 5000); + + sqlite3_stmt* stmt = NULL; + int rc = sqlite3_prepare_v2(g_db, query, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + const char* err_msg = sqlite3_errmsg(g_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(g_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 db_get_total_event_count_ll(long long* out_count) { if (!g_db || !out_count) return -1; @@ -654,6 +866,47 @@ int db_store_config_event(const cJSON* event) { return (rc == SQLITE_DONE) ? 0 : -1; } +int 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) { + if (!g_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(g_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(g_db); + sqlite3_finalize(stmt); + + if (out_step_rc) { + *out_step_rc = step_rc; + } + if (out_extended_errcode) { + *out_extended_errcode = extended_errcode; + } + + return 0; +} + int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) { if (!g_db || !out_min_created_at || !out_max_created_at) return -1; @@ -676,6 +929,86 @@ int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_c return 0; } +int db_event_id_exists(const char* event_id, int* out_exists) { + if (!g_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(g_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* db_retrieve_event_by_id(const char* event_id) { + if (!g_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(g_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* db_get_latest_event_pubkey_for_kind_dup(int kind) { + if (!g_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(g_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 db_get_config_row_count(int* out_count) { if (!g_db || !out_count) return -1; @@ -695,6 +1028,92 @@ int db_get_config_row_count(int* out_count) { return 0; } +int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) { + if (!g_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(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(g_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(g_db)); + } + } + } + tag_index++; + } + + sqlite3_finalize(stmt); + return 0; +} + +int db_populate_event_tags_from_existing(void) { + if (!g_db) return -1; + + sqlite3_stmt* check_stmt = NULL; + if (sqlite3_prepare_v2(g_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(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) return -1; + + sqlite3_exec(g_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) { + db_store_event_tags_cjson(event_id, tags); + cJSON_Delete(tags); + event_count++; + } + } + } + + sqlite3_finalize(stmt); + sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + + DEBUG_INFO("Populated event_tags for %d events", event_count); + return 0; +} + int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1; @@ -745,3 +1164,46 @@ int db_count_wot_whitelist_rules(void) { } return count; } + +int db_table_exists(const char* table_name, int* out_exists) { + if (!g_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(g_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* db_get_schema_version_dup(void) { + if (!g_db) return NULL; + + const char* sql = "SELECT value FROM schema_info WHERE key = 'version'"; + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_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 db_exec_sql(const char* sql) { + if (!g_db || !sql) return -1; + + int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL); + return (rc == SQLITE_OK) ? 0 : -1; +} diff --git a/src/db_ops.h b/src/db_ops.h index 158c300..01da484 100644 --- a/src/db_ops.h +++ b/src/db_ops.h @@ -9,6 +9,21 @@ // Generic helpers int db_is_available(void); sqlite3* db_get_handle(void); +const char* db_last_error(void); +const char* db_get_database_path(void); + +// Generic statement helpers (Phase 1 migration) +int db_prepare(const char* sql, sqlite3_stmt** out_stmt); +int db_bind_text_param(sqlite3_stmt* stmt, int index, const char* value); +int db_bind_int_param(sqlite3_stmt* stmt, int index, int value); +int db_bind_int64_param(sqlite3_stmt* stmt, int index, long long value); +int db_step_stmt(sqlite3_stmt* stmt); +int db_reset_stmt(sqlite3_stmt* stmt); +const char* db_column_text_value(sqlite3_stmt* stmt, int col); +int db_column_int_value(sqlite3_stmt* stmt, int col); +long long db_column_int64_value(sqlite3_stmt* stmt, int col); +double db_column_double_value(sqlite3_stmt* stmt, int col); +void db_finalize_stmt(sqlite3_stmt* stmt); // Subscription logging int db_log_subscription_created(const char* sub_id, const char* wsi_ptr, @@ -32,6 +47,9 @@ int db_count_active_whitelist_rules(void); // Generic prepared COUNT helper int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count); +char* 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); // Monitoring/stat helpers (Phase 1 api.c migration) int db_get_total_event_count_ll(long long* out_count); @@ -51,16 +69,32 @@ int db_store_relay_private_key_hex(const char* relay_privkey_hex); char* db_get_relay_private_key_hex_dup(void); int db_store_config_event(const cJSON* event); -// Event timestamp helpers +// Event storage/timestamp/retrieval helpers +int 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); int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at); +int db_event_id_exists(const char* event_id, int* out_exists); +cJSON* db_retrieve_event_by_id(const char* event_id); +char* db_get_latest_event_pubkey_for_kind_dup(int kind); // Config table helpers int db_get_config_row_count(int* out_count); +// Event tag helpers +int db_store_event_tags_cjson(const char* event_id, const cJSON* tags); +int db_populate_event_tags_from_existing(void); + // Auth/WoT rule helpers int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); int db_delete_wot_whitelist_rules(void); int db_count_wot_whitelist_rules(void); +// Schema/DDL helpers +int db_table_exists(const char* table_name, int* out_exists); +char* db_get_schema_version_dup(void); +int db_exec_sql(const char* sql); + #endif // DB_OPS_H diff --git a/src/main.c b/src/main.c index c3fdcbc..7ea6b62 100644 --- a/src/main.c +++ b/src/main.c @@ -445,64 +445,41 @@ int init_database(const char* database_path_override) { // DEBUG_GUARD_END // Check if database is already initialized by looking for the events table - const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'"; - sqlite3_stmt* check_stmt; - rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); - if (rc == SQLITE_OK) { - int has_events_table = (sqlite3_step(check_stmt) == SQLITE_ROW); - sqlite3_finalize(check_stmt); + int has_events_table = 0; + if (db_table_exists("events", &has_events_table) == 0) { if (has_events_table) { // Check existing schema version and migrate if needed - const char* version_sql = "SELECT value FROM schema_info WHERE key = 'version'"; - sqlite3_stmt* version_stmt; - const char* db_version = NULL; + char* db_version = db_get_schema_version_dup(); int needs_migration = 0; - if (sqlite3_prepare_v2(g_db, version_sql, -1, &version_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(version_stmt) == SQLITE_ROW) { - db_version = (char*)sqlite3_column_text(version_stmt, 0); - - // Check if migration is needed - if (!db_version || strcmp(db_version, "5") == 0) { - needs_migration = 1; - } else if (strcmp(db_version, "6") == 0) { - // Database is at schema version v6 (compatible) - } else if (strcmp(db_version, "7") == 0) { - // Database is at schema version v7 (compatible) - } else if (strcmp(db_version, "8") == 0) { - // Database is at schema version v8 (compatible) - } else if (strcmp(db_version, "9") == 0) { - // Database is at schema version v9 (compatible) - } else if (strcmp(db_version, "10") == 0) { - // Database is at schema version v10 (compatible) - } else if (strcmp(db_version, EMBEDDED_SCHEMA_VERSION) == 0) { - // Database is at current schema version - } else { - char warning_msg[256]; - snprintf(warning_msg, sizeof(warning_msg), "Unknown database schema version: %s (expected %s)", - db_version, EMBEDDED_SCHEMA_VERSION); - DEBUG_WARN(warning_msg); - } - } else { - needs_migration = 1; - } - sqlite3_finalize(version_stmt); - } else { + // Check if migration is needed + if (!db_version || strcmp(db_version, "5") == 0) { needs_migration = 1; + } else if (strcmp(db_version, "6") == 0) { + // Database is at schema version v6 (compatible) + } else if (strcmp(db_version, "7") == 0) { + // Database is at schema version v7 (compatible) + } else if (strcmp(db_version, "8") == 0) { + // Database is at schema version v8 (compatible) + } else if (strcmp(db_version, "9") == 0) { + // Database is at schema version v9 (compatible) + } else if (strcmp(db_version, "10") == 0) { + // Database is at schema version v10 (compatible) + } else if (strcmp(db_version, EMBEDDED_SCHEMA_VERSION) == 0) { + // Database is at current schema version + } else { + char warning_msg[256]; + snprintf(warning_msg, sizeof(warning_msg), "Unknown database schema version: %s (expected %s)", + db_version, EMBEDDED_SCHEMA_VERSION); + DEBUG_WARN(warning_msg); } // Perform migration if needed if (needs_migration) { // Check if auth_rules table already exists - const char* check_auth_rules_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='auth_rules'"; - sqlite3_stmt* check_stmt; int has_auth_rules = 0; - - if (sqlite3_prepare_v2(g_db, check_auth_rules_sql, -1, &check_stmt, NULL) == SQLITE_OK) { - has_auth_rules = (sqlite3_step(check_stmt) == SQLITE_ROW); - sqlite3_finalize(check_stmt); - } + (void)db_table_exists("auth_rules", &has_auth_rules); if (!has_auth_rules) { // Add auth_rules table matching sql_schema.h @@ -519,14 +496,9 @@ int init_database(const char* database_path_override) { " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" ");"; - char* error_msg = NULL; - int rc = sqlite3_exec(g_db, create_auth_rules_sql, NULL, NULL, &error_msg); - if (rc != SQLITE_OK) { - char error_log[512]; - snprintf(error_log, sizeof(error_log), "Failed to create auth_rules table: %s", - error_msg ? error_msg : "unknown error"); - DEBUG_ERROR(error_log); - if (error_msg) sqlite3_free(error_msg); + if (db_exec_sql(create_auth_rules_sql) != 0) { + DEBUG_ERROR("Failed to create auth_rules table"); + if (db_version) free(db_version); return -1; } @@ -536,14 +508,9 @@ int init_database(const char* database_path_override) { "CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);" "CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);"; - char* index_error_msg = NULL; - int index_rc = sqlite3_exec(g_db, create_auth_rules_indexes_sql, NULL, NULL, &index_error_msg); - if (index_rc != SQLITE_OK) { - char index_error_log[512]; - snprintf(index_error_log, sizeof(index_error_log), "Failed to create auth_rules indexes: %s", - index_error_msg ? index_error_msg : "unknown error"); - DEBUG_ERROR(index_error_log); - if (index_error_msg) sqlite3_free(index_error_msg); + if (db_exec_sql(create_auth_rules_indexes_sql) != 0) { + DEBUG_ERROR("Failed to create auth_rules indexes"); + if (db_version) free(db_version); return -1; } } else { @@ -555,32 +522,24 @@ int init_database(const char* database_path_override) { "INSERT OR REPLACE INTO schema_info (key, value, updated_at) " "VALUES ('version', '6', strftime('%s', 'now'))"; - char* error_msg = NULL; - int rc = sqlite3_exec(g_db, update_version_sql, NULL, NULL, &error_msg); - if (rc != SQLITE_OK) { - char error_log[512]; - snprintf(error_log, sizeof(error_log), "Failed to update schema version: %s", - error_msg ? error_msg : "unknown error"); - DEBUG_ERROR(error_log); - if (error_msg) sqlite3_free(error_msg); + if (db_exec_sql(update_version_sql) != 0) { + DEBUG_ERROR("Failed to update schema version"); + if (db_version) free(db_version); return -1; } + if (db_version) { + free(db_version); + } + } else if (db_version) { + free(db_version); } } else { // Initialize database schema using embedded SQL // Execute the embedded schema SQL - char* error_msg = NULL; - rc = sqlite3_exec(g_db, EMBEDDED_SCHEMA_SQL, NULL, NULL, &error_msg); - if (rc != SQLITE_OK) { - char error_log[512]; - snprintf(error_log, sizeof(error_log), "Failed to initialize database schema: %s", - error_msg ? error_msg : "unknown error"); - DEBUG_ERROR(error_log); - if (error_msg) { - sqlite3_free(error_msg); - } + if (db_exec_sql(EMBEDDED_SCHEMA_SQL) != 0) { + DEBUG_ERROR("Failed to initialize database schema"); return -1; } @@ -591,14 +550,8 @@ int init_database(const char* database_path_override) { } // Enable WAL mode for better concurrency and crash recovery - char* wal_error = NULL; - rc = sqlite3_exec(g_db, "PRAGMA journal_mode=WAL;", NULL, NULL, &wal_error); - if (rc != SQLITE_OK) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to enable WAL mode: %s", - wal_error ? wal_error : "unknown error"); - DEBUG_WARN(error_msg); - if (wal_error) sqlite3_free(wal_error); + if (db_exec_sql("PRAGMA journal_mode=WAL;") != 0) { + DEBUG_WARN("Failed to enable WAL mode"); // Continue anyway - WAL mode is optional } else { DEBUG_LOG("SQLite WAL mode enabled"); @@ -611,11 +564,8 @@ int init_database(const char* database_path_override) { if (mmap_size > 0) { char mmap_pragma[64]; snprintf(mmap_pragma, sizeof(mmap_pragma), "PRAGMA mmap_size=%ld;", mmap_size); - char* mmap_error = NULL; - rc = sqlite3_exec(g_db, mmap_pragma, NULL, NULL, &mmap_error); - if (rc != SQLITE_OK) { - DEBUG_WARN("Failed to set mmap_size: %s", mmap_error ? mmap_error : "unknown"); - if (mmap_error) sqlite3_free(mmap_error); + if (db_exec_sql(mmap_pragma) != 0) { + DEBUG_WARN("Failed to set mmap_size"); } else { DEBUG_LOG("SQLite mmap_size set to %ld bytes", mmap_size); } @@ -628,11 +578,8 @@ int init_database(const char* database_path_override) { char cache_pragma[64]; // Use negative value so SQLite interprets it as KB rather than page count snprintf(cache_pragma, sizeof(cache_pragma), "PRAGMA cache_size=-%d;", cache_size_kb > 0 ? cache_size_kb : -cache_size_kb); - char* cache_error = NULL; - rc = sqlite3_exec(g_db, cache_pragma, NULL, NULL, &cache_error); - if (rc != SQLITE_OK) { - DEBUG_WARN("Failed to set cache_size: %s", cache_error ? cache_error : "unknown"); - if (cache_error) sqlite3_free(cache_error); + if (db_exec_sql(cache_pragma) != 0) { + DEBUG_WARN("Failed to set cache_size"); } else { DEBUG_LOG("SQLite cache_size set to %d KB", cache_size_kb); } @@ -649,14 +596,8 @@ void close_database() { if (g_db) { // Perform WAL checkpoint to minimize stale files on next startup DEBUG_LOG("Performing WAL checkpoint before database close"); - char* checkpoint_error = NULL; - int rc = sqlite3_exec(g_db, "PRAGMA wal_checkpoint(TRUNCATE);", NULL, NULL, &checkpoint_error); - if (rc != SQLITE_OK) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "WAL checkpoint warning: %s", - checkpoint_error ? checkpoint_error : "unknown error"); - DEBUG_WARN(error_msg); - if (checkpoint_error) sqlite3_free(checkpoint_error); + if (db_exec_sql("PRAGMA wal_checkpoint(TRUNCATE);") != 0) { + DEBUG_WARN("WAL checkpoint warning"); } sqlite3_close(g_db); @@ -731,43 +672,7 @@ const char* extract_d_tag_value(cJSON* tags) { // Insert denormalized tags into event_tags table for fast indexed lookups int store_event_tags(const char* event_id, cJSON* tags) { - if (!g_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; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(g_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(g_db)); - } - } - } - tag_index++; - } - - sqlite3_finalize(stmt); - return 0; + return db_store_event_tags_cjson(event_id, tags); } // Store event in database @@ -825,40 +730,31 @@ int store_event(cJSON* event) { return -1; } - // Prepare SQL statement for event insertion - const char* sql = - "INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags, event_json) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare event insert statement"); + int rc = SQLITE_ERROR; + int extended_errcode = 0; + if (db_insert_event_with_json(cJSON_GetStringValue(id), + cJSON_GetStringValue(pubkey), + (long long)cJSON_GetNumberValue(created_at), + (int)cJSON_GetNumberValue(kind), + event_type_to_string(type), + cJSON_GetStringValue(content), + cJSON_GetStringValue(sig), + tags_json, + event_json, + &rc, + &extended_errcode) != 0) { + DEBUG_ERROR("Failed to execute event insert operation"); free(tags_json); + free(event_json); return -1; } - - // Bind parameters - sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey), -1, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at)); - sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind)); - sqlite3_bind_text(stmt, 5, event_type_to_string(type), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT); - - // Execute statement - rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { - const char* err_msg = sqlite3_errmsg(g_db); - int extended_errcode = sqlite3_extended_errcode(g_db); + const char* err_msg = db_last_error(); if (rc != SQLITE_CONSTRAINT) { DEBUG_ERROR("INSERT failed: rc=%d, extended_errcode=%d, msg=%s", rc, extended_errcode, err_msg); } } - sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { if (rc == SQLITE_CONSTRAINT) { @@ -888,7 +784,7 @@ int store_event(cJSON* event) { return 0; // Not an error, just duplicate } char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db)); + snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", db_last_error()); DEBUG_ERROR(error_msg); free(tags_json); free(event_json); @@ -929,60 +825,16 @@ int store_event(cJSON* event) { int event_id_exists_in_db(const char* event_id) { if (!g_db || !event_id || strlen(event_id) != 64) return 0; - sqlite3_stmt* stmt; - const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1"; - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; - - sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC); - int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; - sqlite3_finalize(stmt); + int exists = 0; + if (db_event_id_exists(event_id, &exists) != 0) { + return 0; + } return exists; } // Populate event_tags from existing events (run once at startup) int populate_event_tags_from_existing(void) { - if (!g_db) return -1; - - // Check if event_tags is already populated - sqlite3_stmt* check_stmt; - sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL); - 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; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) return -1; - - // Use a transaction for bulk insert performance - sqlite3_exec(g_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) { - store_event_tags(event_id, tags); - cJSON_Delete(tags); - event_count++; - } - } - } - - sqlite3_finalize(stmt); - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); - - DEBUG_INFO("Populated event_tags for %d events", event_count); - return 0; + return db_populate_event_tags_from_existing(); } ///////////////////////////////////////////////////////////////////////////////////////// @@ -992,48 +844,7 @@ int populate_event_tags_from_existing(void) { ///////////////////////////////////////////////////////////////////////////////////////// cJSON* retrieve_event(const char* event_id) { - if (!g_db || !event_id) { - return NULL; - } - - const char* sql = - "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != 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(); - - cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); - cJSON_AddStringToObject(event, "pubkey", (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", (char*)sqlite3_column_text(stmt, 4)); - cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); - - // Parse tags JSON - const char* tags_json = (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; + return db_retrieve_event_by_id(event_id); } @@ -1549,10 +1360,10 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru // Execute query and send events sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + int rc = db_prepare(sql, &stmt); if (rc != SQLITE_OK) { char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to prepare subscription query: %s", sqlite3_errmsg(g_db)); + snprintf(error_msg, sizeof(error_msg), "Failed to prepare subscription query: %s", db_last_error()); DEBUG_ERROR(error_msg); // Log the failed query so we can see what SQL was generated @@ -1565,7 +1376,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru timestamp, sub_id, pss ? pss->client_ip : "N/A", - sqlite3_errmsg(g_db), + db_last_error(), sql); fflush(stderr); } @@ -1580,7 +1391,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru // Bind parameters for (int i = 0; i < bind_param_count; i++) { - sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT); + db_bind_text_param(stmt, i + 1, bind_params[i]); } // Cache config values outside the row loop (performance fix) @@ -1588,7 +1399,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru int filter_responses = get_config_bool("expiration_filter", 1); int row_count = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { + while (db_step_stmt(stmt) == SQLITE_ROW) { row_count++; // Track rows returned for abuse detection @@ -1597,7 +1408,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru } // Get pre-serialized event JSON (no reconstruction needed!) - const char* event_json_str = (char*)sqlite3_column_text(stmt, 0); + const char* event_json_str = db_column_text_value(stmt, 0); if (!event_json_str) { DEBUG_ERROR("Event has NULL event_json field"); continue; @@ -1645,7 +1456,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru events_sent++; } - sqlite3_finalize(stmt); + db_finalize_stmt(stmt); // Stop query timing and log clock_gettime(CLOCK_MONOTONIC, &query_end); diff --git a/src/main.h b/src/main.h index 8e558e4..cbc5df3 100644 --- a/src/main.h +++ b/src/main.h @@ -13,8 +13,8 @@ // Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define CRELAY_VERSION_MAJOR 2 #define CRELAY_VERSION_MINOR 0 -#define CRELAY_VERSION_PATCH 2 -#define CRELAY_VERSION "v2.0.2" +#define CRELAY_VERSION_PATCH 3 +#define CRELAY_VERSION "v2.0.3" // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/src/websockets.c b/src/websockets.c index 7dfe12d..036b41b 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -10,7 +10,6 @@ #include #include #include -#include // Include libwebsockets after pthread.h to ensure pthread_rwlock_t is defined #include