#include "layout_store.h" #include "viz/graph_types.h" #include #include #include #include #include #include namespace ge { namespace { sqlite3* g_db = nullptr; } // namespace // FNV1a 64 sobre el path. No requiere normalizacion canonica (el caller pasa // el path tal cual lo recibio en argv) — basta con que el mismo path produzca // el mismo hash entre runs. uint64_t compute_graph_hash(const char* path) { if (!path) return 0; uint64_t h = 1469598103934665603ull; // FNV offset while (*path) { h ^= (uint64_t)(unsigned char)(*path++); h *= 1099511628211ull; // FNV prime } return h; } bool layout_store_open(const char* db_path) { if (g_db) return true; if (!db_path) return false; int rc = sqlite3_open(db_path, &g_db); if (rc != SQLITE_OK) { std::fprintf(stderr, "[layout_store] sqlite3_open failed: %s\n", sqlite3_errmsg(g_db)); if (g_db) sqlite3_close(g_db); g_db = nullptr; return false; } const char* schema = "CREATE TABLE IF NOT EXISTS layouts (" " graph_hash TEXT NOT NULL," " node_id TEXT NOT NULL," " x REAL NOT NULL," " y REAL NOT NULL," " pinned INTEGER NOT NULL DEFAULT 0," " updated_at INTEGER NOT NULL," " PRIMARY KEY(graph_hash, node_id)" ");"; char* err = nullptr; rc = sqlite3_exec(g_db, schema, nullptr, nullptr, &err); if (rc != SQLITE_OK) { std::fprintf(stderr, "[layout_store] schema error: %s\n", err ? err : "(null)"); sqlite3_free(err); sqlite3_close(g_db); g_db = nullptr; return false; } return true; } void layout_store_close() { if (g_db) { sqlite3_close(g_db); g_db = nullptr; } } namespace { std::string hash_to_hex(uint64_t h) { char buf[17]; std::snprintf(buf, sizeof(buf), "%016llx", (unsigned long long)h); return std::string(buf); } std::string node_user_to_hex(uint64_t u) { char buf[17]; std::snprintf(buf, sizeof(buf), "%016llx", (unsigned long long)u); return std::string(buf); } } // namespace int layout_store_save(uint64_t graph_hash, const GraphData& graph) { if (!g_db || graph.node_count <= 0) return 0; sqlite3_exec(g_db, "BEGIN", nullptr, nullptr, nullptr); const char* sql = "INSERT INTO layouts(graph_hash, node_id, x, y, pinned, updated_at) " "VALUES(?, ?, ?, ?, ?, ?) " "ON CONFLICT(graph_hash, node_id) DO UPDATE SET " " x=excluded.x, y=excluded.y, pinned=excluded.pinned, " " updated_at=excluded.updated_at;"; sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::fprintf(stderr, "[layout_store] prepare save failed: %s\n", sqlite3_errmsg(g_db)); sqlite3_exec(g_db, "ROLLBACK", nullptr, nullptr, nullptr); return -1; } std::string ghex = hash_to_hex(graph_hash); int written = 0; int64_t now = (int64_t)std::time(nullptr); for (int i = 0; i < graph.node_count; ++i) { const GraphNode& n = graph.nodes[i]; if (n.user_data == 0) continue; // sin id estable: no persistible std::string nhex = node_user_to_hex(n.user_data); sqlite3_bind_text(stmt, 1, ghex.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, nhex.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 3, (double)n.x); sqlite3_bind_double(stmt, 4, (double)n.y); sqlite3_bind_int(stmt, 5, (n.flags & NF_PINNED) ? 1 : 0); sqlite3_bind_int64(stmt, 6, now); rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) ++written; sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } sqlite3_finalize(stmt); sqlite3_exec(g_db, "COMMIT", nullptr, nullptr, nullptr); return written; } int layout_store_load(uint64_t graph_hash, GraphData& graph) { if (!g_db || graph.node_count <= 0) return 0; const char* sql = "SELECT node_id, x, y, pinned FROM layouts WHERE graph_hash = ?;"; sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::fprintf(stderr, "[layout_store] prepare load failed: %s\n", sqlite3_errmsg(g_db)); return -1; } std::string ghex = hash_to_hex(graph_hash); sqlite3_bind_text(stmt, 1, ghex.c_str(), -1, SQLITE_TRANSIENT); int touched = 0; while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { const unsigned char* nid = sqlite3_column_text(stmt, 0); if (!nid) continue; uint64_t u = std::strtoull((const char*)nid, nullptr, 16); int idx = graph.find_node_by_user_data(u); if (idx < 0) continue; GraphNode& n = graph.nodes[idx]; n.x = (float)sqlite3_column_double(stmt, 1); n.y = (float)sqlite3_column_double(stmt, 2); if (sqlite3_column_int(stmt, 3)) n.flags |= NF_PINNED; else n.flags &= ~NF_PINNED; ++touched; } sqlite3_finalize(stmt); return touched; } } // namespace ge