Files
graph_explorer/layout_store.cpp
T
egutierrez b767b5b85e feat: graph_explorer app — agnostic operations.db viewer (issue 0049k)
App C++ ImGui que abre cualquier operations.db del registry y lo visualiza
como grafo con shapes/iconos/layouts/filtros/labels.

Composicion del registry:
- viz/graph_renderer + graph_force_layout(_gpu) + graph_layouts +
  graph_viewport + graph_labels + graph_icons + graph_sources
- core: toolbar, modal_dialog, select, text_input, tree_view, page_header,
  fullscreen_window, button, badge, empty_state

Capas:
- data.{h,cpp}    — dispatcher GraphLoadFn (operations hoy; json/graphml manana).
- types_registry.{h,cpp} — parser YAML minimal + tabler_codepoint_by_name +
  apply_types_yaml + IconAtlas builder.
- views.{h,cpp}   — Toolbar, Legend, Inspector, Stats, modal Filters/Open.
- layout_store.{h,cpp} — graph_explorer.db SQLite con tabla layouts(graph_hash,
  node_id, x, y, pinned, updated_at). UPSERT por nodo.
- main.cpp        — CLI (--input/--types/--layout) + fn::run_app + bucle
  force layout (CPU/GPU toggle) + render con 3 columnas (Legend / Viewport /
  Inspector+Stats).

examples/types.yaml: 10 entidades OSINT (Person/Email/Domain/Phone/Org/IBAN/
Account/Document/Address/Url) + 5 relaciones (owns/knows/located_in/
transfers_to/member_of) con shapes Tabler reales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:13:59 +02:00

173 lines
5.2 KiB
C++

#include "layout_store.h"
#include "viz/graph_types.h"
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <string>
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