feat(table-node): DuckDB foundation + render colapsado (issue 0010)

- tableview.{h,cpp}: capa C sobre DuckDB v1.1.3.
  * tableview_smoke_test (SELECT 42).
  * tableview_count (con sql_filter opcional).
  * tableview_page (LEFT JOIN sobre ops.entities via ATTACH para flag promoted).
  * tableview_create (inserta entidad type_ref='Table' con metadata pointer).
  * tableview_refresh_counts (lee Table entities, count cada DuckDB y cachea
    por user_data hash).
  * tableview_resolve_path (rel a dirname(ops_db) o absoluto).
- AppState::table_node_counts cache, refrescado tras load_input y mutaciones.
- views_table_overlay: rectangulo redondeado overlay ("Table  N") encima
  de cada nodo type_ref='Table'. Sigue camara via cam_x/cam_y/zoom.
- main.cpp:
  * --test-duckdb <path> smoke (SELECT 42).
  * --test-tableview <path> bulk test (1M rows count + page offset).
  * Refresh de counts tras load + reload_after_mutation.
  * Llamada a views_table_overlay despues de graph_labels_draw.
- CMakeLists.txt: link DuckDB::DuckDB + duckdb_copy_runtime.

Smoke tests:
- 1M rows count + page(offset=500k, limit=10) en 0.65 s end-to-end.
- Operations.db con un nodo Table apuntando a duckdb 1M filas: refresh
  reporta correctamente "1 tables, 1000000 total rows".
This commit is contained in:
2026-05-01 01:24:25 +02:00
parent 20d8bbf360
commit 082008bc00
7 changed files with 597 additions and 3 deletions
+74 -1
View File
@@ -28,6 +28,9 @@
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
#include "tableview.h"
#include "duckdb.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -253,6 +256,16 @@ static bool load_input() {
ge::views_reset_visibility(g_app);
ge::views_apply_visibility(g_app);
// Cache de conteos de Table nodes (issue 0010).
if (g_input.uri) {
ge::tableview_refresh_counts(g_input.uri, &g_app.table_node_counts);
int64_t total_rows = 0;
for (auto& kv : g_app.table_node_counts) total_rows += kv.second;
std::fprintf(stdout,
"[graph_explorer] table counts refreshed: %zu tables, %lld total rows\n",
g_app.table_node_counts.size(), (long long)total_rows);
}
// Cache de la vista tabla (issue 0004) — pull bulk + neighbors desde grafo.
{
std::vector<ge::EntityRowSnapshot> snap;
@@ -689,6 +702,9 @@ static void render() {
ge::views_reset_visibility(g_app);
ge::views_apply_visibility(g_app);
// Refresh Table node counts (issue 0010).
ge::tableview_refresh_counts(g_input.uri, &g_app.table_node_counts);
// Refresh table cache (issue 0004).
std::vector<ge::EntityRowSnapshot> snap;
if (ge::entity_list_rows(g_input.uri, &snap)) {
@@ -941,6 +957,8 @@ static void render() {
graph::graph_labels_draw(g_graph, g_viewport, g_label_policy,
&get_label_cb, nullptr);
}
// Table node overlay (issue 0010) — encima de las labels.
ge::views_table_overlay(g_app);
}
ImGui::End();
} else {
@@ -997,7 +1015,9 @@ static void usage() {
" graph_explorer --types <types.yaml>\n"
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n"
" graph_explorer --project <slug>\n"
" graph_explorer --test-types-yaml <path> (load+save+reload smoke test)\n");
" graph_explorer --test-types-yaml <path> (load+save+reload smoke test)\n"
" graph_explorer --test-duckdb <path> (open + SELECT 42 smoke test)\n"
" graph_explorer --test-tableview <path> (1M rows count + page test)\n");
}
// Smoke test del parser+writer (issue 0005 round-trip): carga `path`,
@@ -1091,6 +1111,59 @@ int main(int argc, char** argv) {
project_arg = argv[++i];
} else if (std::strcmp(a, "--test-types-yaml") == 0 && i + 1 < argc) {
return test_types_yaml_roundtrip(argv[++i]);
} else if (std::strcmp(a, "--test-duckdb") == 0 && i + 1 < argc) {
const char* p = argv[++i];
if (!ge::tableview_smoke_test(p)) {
std::fprintf(stderr, "[duckdb] smoke test FAILED for %s\n", p);
return 2;
}
std::fprintf(stdout, "[duckdb] smoke test OK (SELECT 42 -> 42) on %s\n", p);
return 0;
} else if (std::strcmp(a, "--test-tableview") == 0 && i + 1 < argc) {
// Crea 1M filas en duckdb_path/people, cuenta y pagina.
const char* p = argv[++i];
std::remove(p); // empezar desde cero
duckdb_database db = nullptr;
duckdb_connection cn = nullptr;
if (duckdb_open(p, &db) == DuckDBError) { std::fprintf(stderr, "open fail\n"); return 2; }
duckdb_connect(db, &cn);
duckdb_result r;
if (duckdb_query(cn,
"CREATE TABLE people AS "
"SELECT range AS id, 'name_' || CAST(range AS VARCHAR) AS name, "
" (range * 7) % 100 AS age FROM range(1000000)", &r) == DuckDBError) {
std::fprintf(stderr, "create fail: %s\n",
duckdb_result_error(&r) ? duckdb_result_error(&r) : "?");
duckdb_destroy_result(&r);
duckdb_disconnect(&cn); duckdb_close(&db); return 2;
}
duckdb_destroy_result(&r);
duckdb_disconnect(&cn); duckdb_close(&db);
int64_t total = 0;
if (!ge::tableview_count(p, "people", nullptr, &total) || total != 1000000) {
std::fprintf(stderr, "[tableview_count] expected 1000000, got %lld\n",
(long long)total);
return 2;
}
std::vector<std::string> cols = { "name", "age" };
std::vector<ge::TablePageRow> page;
if (!ge::tableview_page(p, "people", "id", cols, nullptr,
nullptr, nullptr, 500000, 10, &page)) {
std::fprintf(stderr, "[tableview_page] failed\n");
return 2;
}
if (page.size() != 10) {
std::fprintf(stderr, "[tableview_page] expected 10 rows, got %zu\n",
page.size());
return 2;
}
std::fprintf(stdout,
"[tableview] OK — count=%lld, page[0]={id=%s, name=%s, age=%s}\n",
(long long)total, page[0].id.c_str(),
page[0].values.size() > 0 ? page[0].values[0].c_str() : "",
page[0].values.size() > 1 ? page[0].values[1].c_str() : "");
return 0;
} else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) {
usage();
return 0;