refactor(0036a): rename Table-expanded -> NodeGroups (paperwork)

Rename masivo sin cambio de comportamiento. Habilita 0036b-f que ya
asumen la nueva convencion.

Archivos:
- tableview.{cpp,h} -> node_groups.{cpp,h} (git mv para preservar history)
- CMakeLists.txt: tableview.cpp -> node_groups.cpp

Tipos:
- TableWindowState  -> NodeGroupsWindowState  (views.h)
- TableMetadata     -> NodeGroupsMeta         (node_groups.h)
- TablePageRow      -> NodeGroupsRow          (node_groups.h)

Campos AppState:
- table_windows         -> node_groups_windows
- table_node_counts     -> node_groups_counts
- toggle_expanded_id    -> toggle_nodegroups_id
- want_toggle_expanded  -> want_toggle_nodegroups

Funciones (window por contenedor — NO el panel generico Table):
- tableview_create / count / page / smoke_test / resolve_path /
  refresh_counts / list_columns / get_metadata / set_expanded /
  set_columns / promote_row / demote_row / ingest_file
  -> prefijo node_groups_*
- views_table_window         -> views_node_groups_window
- views_table_windows_sync   -> views_node_groups_windows_sync
- views_table_overlay        -> views_node_groups_overlay

Strings de UI:
- "Expand table" / "Collapse table" -> "Open NodeGroups" / "Close NodeGroups"
- Window title "<icon> <name>" -> "<icon> NodeGroups: <name>"
- Tooltip "(no expanded tables)" -> "(no open NodeGroups)"
- Logs [tableview_*] -> [node_groups_*]

Preservados intencionalmente (no son cambio de identificadores C++):
- CLI flag --test-tableview (cambiarlo seria cambio de behavior publico)
- Valor 'tableview' en columna entities.source (cambiarlo afectaria
  datos persistidos en BD)

NO tocado:
- Panel generico Table (views_table, panel_table, table_rows,
  table_show_all, table_search_buf, table_filter_*, table_col_filters,
  table_active_tab, TableRow, table_filter_group_*, etc.)
- issues/completed/* (historia)

Verificacion:
- Build C++ Linux + Windows: green sin warnings nuevos.
- pytest WSL: 89 passed.
- pytest Windows: 78 passed + 11 skipped.
- git grep audit: solo residuos en issues/ (historia) + CLI flag y
  source DB value preservados.

Refs: issues/0036a-rename-nodegroups.md
This commit is contained in:
2026-05-04 00:43:01 +02:00
parent 441a697abf
commit 810b564127
7 changed files with 163 additions and 161 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ add_imgui_app(graph_explorer
layout_store.cpp
entity_ops.cpp
project_manager.cpp
tableview.cpp
node_groups.cpp
jobs.cpp
enrichers.cpp
chat.cpp
+1 -1
View File
@@ -429,7 +429,7 @@ def cmd_table_list(args) -> None:
def cmd_table_promote(args) -> None:
"""Promociona una fila DuckDB a entidad. Replica tableview_promote_row."""
"""Promociona una fila DuckDB a entidad. Replica node_groups_promote_row."""
cn = _connect(_ops_db())
try:
row = cn.execute(
+52 -52
View File
@@ -33,7 +33,7 @@
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
#include "tableview.h"
#include "node_groups.h"
#include "duckdb.h"
#include <cstdio>
@@ -707,15 +707,15 @@ static bool load_input(bool first_load) {
// Cache de conteos de Table nodes (issue 0010).
if (g_input.uri) {
ge::tableview_refresh_counts(g_input.uri, &g_app.table_node_counts);
ge::node_groups_refresh_counts(g_input.uri, &g_app.node_groups_counts);
int64_t total_rows = 0;
for (auto& kv : g_app.table_node_counts) total_rows += kv.second;
for (auto& kv : g_app.node_groups_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);
g_app.node_groups_counts.size(), (long long)total_rows);
// Sync de windows expandidas (issue 0011) — reabre las que el
// usuario tenia abiertas en la sesion previa (metadata.expanded=true).
ge::views_table_windows_sync(g_app, g_input.uri);
ge::views_node_groups_windows_sync(g_app, g_input.uri);
}
// Cache de la vista tabla (issue 0004) — pull bulk + neighbors desde grafo.
@@ -882,15 +882,15 @@ static void render_context_menu() {
const char* sql_id = ge::entity_index_lookup(g_idx, n.user_data);
if (is_table && sql_id) {
// Determinar estado expanded actual sin ir a BD: mira table_windows.
bool currently_expanded =
g_app.table_windows.find(sql_id) != g_app.table_windows.end();
const char* lbl_exp = currently_expanded
? TI_X " Collapse table"
: TI_TABLE " Expand table";
// Determinar estado actual sin ir a BD: mira node_groups_windows.
bool currently_open =
g_app.node_groups_windows.find(sql_id) != g_app.node_groups_windows.end();
const char* lbl_exp = currently_open
? TI_X " Close NodeGroups"
: TI_TABLE " Open NodeGroups";
if (ImGui::MenuItem(lbl_exp)) {
g_app.want_toggle_expanded = true;
g_app.toggle_expanded_id = sql_id;
g_app.want_toggle_nodegroups = true;
g_app.toggle_nodegroups_id = sql_id;
}
ImGui::Separator();
}
@@ -1580,7 +1580,7 @@ static void render() {
// Reaplica types.yaml + atlas. Sin esto, despues de cualquier
// mutacion los tipos pierden color/shape/icon (todo nodo vuelve a
// circulo gris). Issue: al promover desde tableview el Table
// circulo gris). Issue: al promover desde node_groups el Table
// dejaba de ser cuadrado.
if (!g_app.parsed_types.entities.empty() ||
!g_app.parsed_types.relations.empty()) {
@@ -1592,10 +1592,10 @@ static void render() {
}
// Refresh Table node counts (issue 0010).
ge::tableview_refresh_counts(g_input.uri, &g_app.table_node_counts);
ge::node_groups_refresh_counts(g_input.uri, &g_app.node_groups_counts);
// Sincroniza windows (issue 0011) por si una Table aparecio o desaparecio.
ge::views_table_windows_sync(g_app, g_input.uri);
ge::views_node_groups_windows_sync(g_app, g_input.uri);
// Refresh table cache (issue 0004).
std::vector<ge::EntityRowSnapshot> snap;
@@ -1688,30 +1688,30 @@ static void render() {
}
// ---- Table node UI fase 2 (issue 0011) ----
if (g_app.want_toggle_expanded && !g_app.toggle_expanded_id.empty()
if (g_app.want_toggle_nodegroups && !g_app.toggle_nodegroups_id.empty()
&& !g_input_path.empty()) {
std::string id = g_app.toggle_expanded_id;
bool currently = g_app.table_windows.find(id) != g_app.table_windows.end();
ge::tableview_set_expanded(g_input_path.c_str(), id.c_str(), !currently);
ge::views_table_windows_sync(g_app, g_input_path.c_str());
g_app.want_toggle_expanded = false;
g_app.toggle_expanded_id.clear();
std::string id = g_app.toggle_nodegroups_id;
bool currently = g_app.node_groups_windows.find(id) != g_app.node_groups_windows.end();
ge::node_groups_set_expanded(g_input_path.c_str(), id.c_str(), !currently);
ge::views_node_groups_windows_sync(g_app, g_input_path.c_str());
g_app.want_toggle_nodegroups = false;
g_app.toggle_nodegroups_id.clear();
}
// Cierre via X de la ventana -> bajar expanded en BD.
for (auto it = g_app.table_windows.begin(); it != g_app.table_windows.end(); ) {
for (auto it = g_app.node_groups_windows.begin(); it != g_app.node_groups_windows.end(); ) {
if (!it->second.open && !g_input_path.empty()) {
ge::tableview_set_expanded(g_input_path.c_str(),
ge::node_groups_set_expanded(g_input_path.c_str(),
it->first.c_str(), false);
it = g_app.table_windows.erase(it);
it = g_app.node_groups_windows.erase(it);
} else ++it;
}
// Refrescar la pagina si alguna window esta dirty.
for (auto& kv : g_app.table_windows) {
for (auto& kv : g_app.node_groups_windows) {
auto& w = kv.second;
if (!w.page_dirty) continue;
const auto& m = w.meta;
w.last_error.clear();
bool ok_count = ge::tableview_count(m.duckdb_path_abs.c_str(),
bool ok_count = ge::node_groups_count(m.duckdb_path_abs.c_str(),
m.table_name.c_str(),
m.filter_sql.empty() ? nullptr : m.filter_sql.c_str(),
&w.total_rows);
@@ -1725,14 +1725,14 @@ static void render() {
}
if (m.columns.empty()) {
std::vector<std::string> cols;
if (ge::tableview_list_columns(m.duckdb_path_abs.c_str(),
if (ge::node_groups_list_columns(m.duckdb_path_abs.c_str(),
m.table_name.c_str(), &cols)) {
ge::tableview_set_columns(g_input_path.c_str(),
ge::node_groups_set_columns(g_input_path.c_str(),
m.entity_id.c_str(), cols);
w.meta.columns = cols;
}
}
bool ok_page = ge::tableview_page(m.duckdb_path_abs.c_str(),
bool ok_page = ge::node_groups_page(m.duckdb_path_abs.c_str(),
m.table_name.c_str(), m.id_column.c_str(),
w.meta.columns,
m.filter_sql.empty() ? nullptr : m.filter_sql.c_str(),
@@ -1749,11 +1749,11 @@ static void render() {
}
if (g_app.want_promote_row && !g_app.promote_table_id.empty()
&& !g_input_path.empty()) {
ge::TableMetadata m;
if (ge::tableview_get_metadata(g_input_path.c_str(),
ge::NodeGroupsMeta m;
if (ge::node_groups_get_metadata(g_input_path.c_str(),
g_app.promote_table_id.c_str(), &m)) {
char new_id[128] = {};
if (ge::tableview_promote_row(g_input_path.c_str(),
if (ge::node_groups_promote_row(g_input_path.c_str(),
g_app.promote_table_id.c_str(),
m.duckdb_path_abs.c_str(),
m.table_name.c_str(),
@@ -1763,8 +1763,8 @@ static void render() {
new_id, sizeof(new_id))) {
std::fprintf(stdout, "[promote] %s -> %s\n",
g_app.promote_row_id.c_str(), new_id);
auto it = g_app.table_windows.find(g_app.promote_table_id);
if (it != g_app.table_windows.end()) it->second.page_dirty = true;
auto it = g_app.node_groups_windows.find(g_app.promote_table_id);
if (it != g_app.node_groups_windows.end()) it->second.page_dirty = true;
reload_after_mutation();
g_app.want_focus_entity = true;
g_app.focus_entity_id = new_id;
@@ -1776,10 +1776,10 @@ static void render() {
}
if (g_app.want_demote_entity && !g_app.demote_entity_id.empty()
&& !g_input_path.empty()) {
if (ge::tableview_demote_row(g_input_path.c_str(),
if (ge::node_groups_demote_row(g_input_path.c_str(),
g_app.demote_entity_id.c_str())) {
std::fprintf(stdout, "[demote] %s\n", g_app.demote_entity_id.c_str());
for (auto& kv : g_app.table_windows) kv.second.page_dirty = true;
for (auto& kv : g_app.node_groups_windows) kv.second.page_dirty = true;
reload_after_mutation();
}
g_app.want_demote_entity = false;
@@ -1806,17 +1806,17 @@ static void render() {
if (g_app.want_import) {
g_app.want_import = false;
g_app.import_error.clear();
std::string duck_abs = ge::tableview_resolve_path(
std::string duck_abs = ge::node_groups_resolve_path(
g_input_path.c_str(), g_app.import_duckdb_buf);
std::string err;
if (!ge::tableview_ingest_file(duck_abs.c_str(),
if (!ge::node_groups_ingest_file(duck_abs.c_str(),
g_app.import_path_buf,
g_app.import_table_buf,
ge::INGEST_AUTO, &err)) {
g_app.import_error = "Ingest failed: " + err;
} else {
char new_id[80] = {};
if (ge::tableview_create(g_input_path.c_str(),
if (ge::node_groups_create(g_input_path.c_str(),
g_app.import_table_buf,
g_app.import_duckdb_buf,
g_app.import_table_buf,
@@ -1923,7 +1923,7 @@ static void render() {
if (type_name && std::strcmp(type_name, "Group") == 0) is_group = true;
if (is_group && sql_id) {
// Drill-in: abrir tableview filtrada por group_id = sql_id.
// Drill-in: abrir Table panel filtrado por group_id = sql_id.
g_app.table_filter_group_id = sql_id;
const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx);
g_app.table_filter_group_name = lbl ? lbl : sql_id;
@@ -2007,7 +2007,7 @@ static void render() {
&get_label_cb, nullptr);
}
// Table node overlay (issue 0010) — encima de las labels.
ge::views_table_overlay(g_app);
ge::views_node_groups_overlay(g_app);
}
ImGui::End();
} else {
@@ -2051,7 +2051,7 @@ static void render() {
ge::views_table(g_app);
// Table node windows (issue 0011) — una por Table expandida.
ge::views_table_window(g_app);
ge::views_node_groups_window(g_app);
ge::views_import_dataset_modal(g_app);
// Jobs panel (issue 0026) — flotante, dockeable.
@@ -2181,7 +2181,7 @@ int main(int argc, char** argv) {
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)) {
if (!ge::node_groups_smoke_test(p)) {
std::fprintf(stderr, "[duckdb] smoke test FAILED for %s\n", p);
return 2;
}
@@ -2209,25 +2209,25 @@ int main(int argc, char** argv) {
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",
if (!ge::node_groups_count(p, "people", nullptr, &total) || total != 1000000) {
std::fprintf(stderr, "[node_groups_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,
std::vector<ge::NodeGroupsRow> page;
if (!ge::node_groups_page(p, "people", "id", cols, nullptr,
nullptr, nullptr, 500000, 10, &page)) {
std::fprintf(stderr, "[tableview_page] failed\n");
std::fprintf(stderr, "[node_groups_page] failed\n");
return 2;
}
if (page.size() != 10) {
std::fprintf(stderr, "[tableview_page] expected 10 rows, got %zu\n",
std::fprintf(stderr, "[node_groups_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",
"[node_groups] 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() : "");
+24 -24
View File
@@ -1,4 +1,4 @@
#include "tableview.h"
#include "node_groups.h"
#include "duckdb.h"
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
@@ -62,7 +62,7 @@ bool duck_query_silent(duckdb_connection cn, const char* sql) {
} // namespace
bool tableview_smoke_test(const char* duckdb_path) {
bool node_groups_smoke_test(const char* duckdb_path) {
DuckHandle h;
if (!h.open(duckdb_path)) return false;
duckdb_result r;
@@ -76,7 +76,7 @@ bool tableview_smoke_test(const char* duckdb_path) {
return ok;
}
bool tableview_count(const char* duckdb_path,
bool node_groups_count(const char* duckdb_path,
const char* duck_table,
const char* sql_filter,
int64_t* out)
@@ -94,7 +94,7 @@ bool tableview_count(const char* duckdb_path,
}
duckdb_result r;
if (duckdb_query(h.cn, sql.c_str(), &r) == DuckDBError) {
std::fprintf(stderr, "[tableview_count] %s\n",
std::fprintf(stderr, "[node_groups_count] %s\n",
duckdb_result_error(&r) ? duckdb_result_error(&r) : "?");
duckdb_destroy_result(&r);
return false;
@@ -106,7 +106,7 @@ bool tableview_count(const char* duckdb_path,
return true;
}
bool tableview_page(const char* duckdb_path,
bool node_groups_page(const char* duckdb_path,
const char* duck_table,
const char* id_column,
const std::vector<std::string>& columns,
@@ -114,7 +114,7 @@ bool tableview_page(const char* duckdb_path,
const char* ops_db,
const char* row_type,
int64_t offset, int64_t limit,
std::vector<TablePageRow>* out)
std::vector<NodeGroupsRow>* out)
{
if (!out) return false;
out->clear();
@@ -177,7 +177,7 @@ bool tableview_page(const char* duckdb_path,
duckdb_result r;
if (duckdb_query(h.cn, sel.c_str(), &r) == DuckDBError) {
const char* e = duckdb_result_error(&r);
std::fprintf(stderr, "[tableview_page] FAIL: %s\n SQL: %s\n",
std::fprintf(stderr, "[node_groups_page] FAIL: %s\n SQL: %s\n",
e ? e : "?", sel.c_str());
duckdb_destroy_result(&r);
return false;
@@ -186,7 +186,7 @@ bool tableview_page(const char* duckdb_path,
idx_t cols = duckdb_column_count(&r);
out->reserve((size_t)rows);
for (idx_t row = 0; row < rows; ++row) {
TablePageRow tr;
NodeGroupsRow tr;
// col 0 = id
if (!duckdb_value_is_null(&r, 0, row)) {
char* v = duckdb_value_varchar(&r, 0, row);
@@ -220,7 +220,7 @@ bool tableview_page(const char* duckdb_path,
return true;
}
bool tableview_create(const char* ops_db,
bool node_groups_create(const char* ops_db,
const char* name,
const char* duckdb_path,
const char* duck_table,
@@ -317,7 +317,7 @@ static std::string normalize_path(std::string p) {
return p;
}
std::string tableview_resolve_path(const char* ops_db, const char* maybe_rel) {
std::string node_groups_resolve_path(const char* ops_db, const char* maybe_rel) {
if (!maybe_rel) return "";
if (is_absolute(maybe_rel)) return normalize_path(maybe_rel);
std::string base = dirname_of(ops_db);
@@ -325,7 +325,7 @@ std::string tableview_resolve_path(const char* ops_db, const char* maybe_rel) {
return normalize_path(base + "/" + maybe_rel);
}
bool tableview_refresh_counts(const char* ops_db,
bool node_groups_refresh_counts(const char* ops_db,
std::unordered_map<uint64_t, int64_t>* out)
{
if (!ops_db || !out) return false;
@@ -352,13 +352,13 @@ bool tableview_refresh_counts(const char* ops_db,
const unsigned char* tab_p = sqlite3_column_text(st, 2);
const unsigned char* flt_p = sqlite3_column_text(st, 3);
if (!id_p || !path_p || !tab_p) continue;
std::string abs = tableview_resolve_path(ops_db, (const char*)path_p);
std::string abs = node_groups_resolve_path(ops_db, (const char*)path_p);
int64_t total = 0;
if (!tableview_count(abs.c_str(), (const char*)tab_p,
if (!node_groups_count(abs.c_str(), (const char*)tab_p,
flt_p ? (const char*)flt_p : nullptr,
&total)) {
std::fprintf(stderr,
"[tableview_refresh_counts] count failed for id=%s\n", id_p);
"[node_groups_refresh_counts] count failed for id=%s\n", id_p);
continue;
}
out->emplace(fnv1a64((const char*)id_p), total);
@@ -415,7 +415,7 @@ std::vector<std::string> parse_json_string_array(const char* s) {
} // namespace
bool tableview_list_columns(const char* duckdb_path,
bool node_groups_list_columns(const char* duckdb_path,
const char* duck_table,
std::vector<std::string>* out)
{
@@ -438,11 +438,11 @@ bool tableview_list_columns(const char* duckdb_path,
return true;
}
bool tableview_get_metadata(const char* ops_db, const char* entity_id,
TableMetadata* out)
bool node_groups_get_metadata(const char* ops_db, const char* entity_id,
NodeGroupsMeta* out)
{
if (!ops_db || !entity_id || !out) return false;
*out = TableMetadata{};
*out = NodeGroupsMeta{};
out->entity_id = entity_id;
sqlite3* db = nullptr;
if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
@@ -486,7 +486,7 @@ bool tableview_get_metadata(const char* ops_db, const char* entity_id,
if (out->label_column.empty()) out->label_column = "name";
out->columns = parse_json_string_array(cols_json.c_str());
out->expanded = (exp_json == "true" || exp_json == "1");
out->duckdb_path_abs = tableview_resolve_path(ops_db, out->duckdb_path.c_str());
out->duckdb_path_abs = node_groups_resolve_path(ops_db, out->duckdb_path.c_str());
ok = true;
}
sqlite3_finalize(st);
@@ -521,7 +521,7 @@ bool exec_metadata_patch(const char* ops_db, const char* entity_id,
} // namespace
bool tableview_set_expanded(const char* ops_db, const char* entity_id,
bool node_groups_set_expanded(const char* ops_db, const char* entity_id,
bool expanded)
{
if (!ops_db || !entity_id) return false;
@@ -531,7 +531,7 @@ bool tableview_set_expanded(const char* ops_db, const char* entity_id,
return exec_metadata_patch(ops_db, entity_id, clause.c_str());
}
bool tableview_set_columns(const char* ops_db, const char* entity_id,
bool node_groups_set_columns(const char* ops_db, const char* entity_id,
const std::vector<std::string>& columns)
{
if (!ops_db || !entity_id) return false;
@@ -586,7 +586,7 @@ bool find_existing_promotion(const char* ops_db, const char* duckdb_path,
} // namespace
bool tableview_promote_row(const char* ops_db,
bool node_groups_promote_row(const char* ops_db,
const char* table_entity_id,
const char* duckdb_path,
const char* duck_table,
@@ -729,7 +729,7 @@ bool tableview_promote_row(const char* ops_db,
}
}
bool tableview_demote_row(const char* ops_db, const char* entity_id) {
bool node_groups_demote_row(const char* ops_db, const char* entity_id) {
if (!ops_db || !entity_id) return false;
sqlite3* db = nullptr;
if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
@@ -774,7 +774,7 @@ const char* ingest_func_name(IngestKind k) {
} // namespace
bool tableview_ingest_file(const char* duckdb_path,
bool node_groups_ingest_file(const char* duckdb_path,
const char* file_path,
const char* dest_table,
IngestKind kind,
+26 -26
View File
@@ -4,10 +4,10 @@
#include <unordered_map>
#include <vector>
// Vista tabular respaldada por DuckDB (issue 0010). Cada nodo `Table` del
// grafo apunta via metadata a un archivo `.duckdb` y a una tabla dentro
// de el. Las filas viven en DuckDB; el grafo solo materializa las que
// se "promueven" a entidades (issue 0011).
// NodeGroups — vista tabular respaldada por DuckDB (issue 0010, renombrada
// en 0036a). Cada nodo `Table` del grafo apunta via metadata a un archivo
// `.duckdb` y a una tabla dentro de el. Las filas viven en DuckDB; el grafo
// solo materializa las que se "promueven" a entidades (issue 0011).
//
// Convencion de paths: `metadata.duckdb_path` es relativo al directorio del
// proyecto (la raiz donde vive operations.db). El caller resuelve a path
@@ -15,7 +15,7 @@
namespace ge {
struct TablePageRow {
struct NodeGroupsRow {
std::string id; // valor del id_column en duckdb (key natural)
std::vector<std::string> values; // un valor por columna en `columns[]`
std::string promoted_entity_id; // "" si la fila no esta promovida; sino, ops.entities.id
@@ -25,7 +25,7 @@ struct TablePageRow {
// type_ref='Table' y metadata apuntando al duckdb_path/table_name. Genera
// un id propio. Devuelve false si SQLite falla o si los argumentos basicos
// estan vacios.
bool tableview_create(const char* ops_db,
bool node_groups_create(const char* ops_db,
const char* name,
const char* duckdb_path,
const char* duck_table,
@@ -35,7 +35,7 @@ bool tableview_create(const char* ops_db,
// Cuenta las filas de duckdb_path/duck_table aplicando opcionalmente
// `sql_filter` (clausula WHERE sin la palabra WHERE — vacio = sin filtro).
// Devuelve false en error de IO/parse.
bool tableview_count(const char* duckdb_path,
bool node_groups_count(const char* duckdb_path,
const char* duck_table,
const char* sql_filter,
int64_t* out);
@@ -44,7 +44,7 @@ bool tableview_count(const char* duckdb_path,
// valores de `columns` resueltos a string + el flag `promoted_entity_id`
// computado via LEFT JOIN contra ops.entities (DuckDB attach a SQLite).
// limit clampeado en [1,5000].
bool tableview_page(const char* duckdb_path,
bool node_groups_page(const char* duckdb_path,
const char* duck_table,
const char* id_column,
const std::vector<std::string>& columns,
@@ -52,20 +52,20 @@ bool tableview_page(const char* duckdb_path,
const char* ops_db, // para LEFT JOIN de promovidas
const char* row_type, // discriminante en ops.entities
int64_t offset, int64_t limit,
std::vector<TablePageRow>* out);
std::vector<NodeGroupsRow>* out);
// Smoke test: abre el .duckdb, corre `SELECT 42 AS x` y verifica que
// devuelve la fila esperada. Devuelve true si todo OK.
bool tableview_smoke_test(const char* duckdb_path);
bool node_groups_smoke_test(const char* duckdb_path);
// Resuelve un path posiblemente relativo a la ubicacion de operations.db.
// Si es absoluto (empieza por '/' o '<letra>:' en Windows), se devuelve
// tal cual.
std::string tableview_resolve_path(const char* ops_db, const char* maybe_rel);
std::string node_groups_resolve_path(const char* ops_db, const char* maybe_rel);
// Refresca el cache de conteos de filas por nodo Table. Lee
// type_ref='Table' de operations.db, extrae metadata.duckdb_path/table_name,
// llama a tableview_count y guarda el resultado indexado por
// llama a node_groups_count y guarda el resultado indexado por
// fnv1a64(entity_id) — la misma key que usa graph_sources al setear
// node.user_data, asi que el render puede mirar directo por user_data.
// Si una tabla falla, su entrada NO se inserta y se imprime un warning.
@@ -73,7 +73,7 @@ struct TableCounts {
// user_data hash (fnv1a64 del entity id) -> total filas tras filter_sql.
// -1 indica error/ausencia.
};
bool tableview_refresh_counts(const char* ops_db,
bool node_groups_refresh_counts(const char* ops_db,
std::unordered_map<uint64_t, int64_t>* out);
// ----------------------------------------------------------------------------
@@ -81,11 +81,11 @@ bool tableview_refresh_counts(const char* ops_db,
// ----------------------------------------------------------------------------
// Snapshot de la metadata relevante de un nodo Table. Caller-owned strings.
struct TableMetadata {
struct NodeGroupsMeta {
std::string entity_id;
std::string name;
std::string duckdb_path; // tal como aparece en metadata (relativo o absoluto)
std::string duckdb_path_abs; // resuelto con tableview_resolve_path
std::string duckdb_path_abs; // resuelto con node_groups_resolve_path
std::string table_name;
std::string row_type;
std::string id_column;
@@ -98,17 +98,17 @@ struct TableMetadata {
// Lee la metadata del nodo Table (entidad type_ref='Table' con id=`entity_id`).
// Devuelve false si no existe o falla el JSON. Aplica defaults razonables a
// los campos faltantes (id_column='id', label_column='name').
bool tableview_get_metadata(const char* ops_db, const char* entity_id,
TableMetadata* out);
bool node_groups_get_metadata(const char* ops_db, const char* entity_id,
NodeGroupsMeta* out);
// Persiste el flag `expanded` en la metadata. Idempotente. Devuelve false
// en error de IO/SQL.
bool tableview_set_expanded(const char* ops_db, const char* entity_id,
bool node_groups_set_expanded(const char* ops_db, const char* entity_id,
bool expanded);
// Sobrescribe el array `columns` en la metadata. Llamar tras editar columnas
// desde la UI o tras tableview_create cuando se descubren columnas.
bool tableview_set_columns(const char* ops_db, const char* entity_id,
// desde la UI o tras node_groups_create cuando se descubren columnas.
bool node_groups_set_columns(const char* ops_db, const char* entity_id,
const std::vector<std::string>& columns);
// Promueve una fila de DuckDB a entidad del grafo. Idempotente: si ya existe
@@ -123,7 +123,7 @@ bool tableview_set_columns(const char* ops_db, const char* entity_id,
// Si table_entity_id no es nulo/vacio, inserta tambien una relacion
// CONTAINS_ROW (idempotente) entre la tabla y la nueva entidad para que el
// viewport pinte la arista de pertenencia.
bool tableview_promote_row(const char* ops_db,
bool node_groups_promote_row(const char* ops_db,
const char* table_entity_id,
const char* duckdb_path,
const char* duck_table,
@@ -134,9 +134,9 @@ bool tableview_promote_row(const char* ops_db,
// Borra la entidad. La fila DuckDB sigue intacta. Devuelve true si la fila
// no existia (no-op idempotente).
bool tableview_demote_row(const char* ops_db, const char* entity_id);
bool node_groups_demote_row(const char* ops_db, const char* entity_id);
// Tipos de fichero soportados por tableview_ingest_file.
// Tipos de fichero soportados por node_groups_ingest_file.
enum IngestKind {
INGEST_AUTO = 0, // detecta por extension
INGEST_CSV = 1,
@@ -147,15 +147,15 @@ enum IngestKind {
// Importa un fichero CSV/Parquet/JSON al `.duckdb`. Crea el .duckdb si no
// existe. Si la tabla destino existe, falla (no sobrescribe — explicit fail).
// Por defecto INGEST_AUTO inspecciona la extension del path.
bool tableview_ingest_file(const char* duckdb_path,
bool node_groups_ingest_file(const char* duckdb_path,
const char* file_path,
const char* dest_table,
IngestKind kind,
std::string* out_error);
// Lista los nombres de columna de la tabla DuckDB. Para popular la lista
// `columns` por defecto en tableview_create.
bool tableview_list_columns(const char* duckdb_path,
// `columns` por defecto en node_groups_create.
bool node_groups_list_columns(const char* duckdb_path,
const char* duck_table,
std::vector<std::string>* out);
+24 -24
View File
@@ -306,15 +306,15 @@ void views_toolbar(AppState& app) {
{
char btn[64];
std::snprintf(btn, sizeof(btn), TI_TABLE " Tables (%zu)",
app.table_windows.size());
app.node_groups_windows.size());
if (button(btn, ButtonVariant::Subtle)) {
ImGui::OpenPopup("##tables_menu");
}
if (ImGui::BeginPopup("##tables_menu")) {
if (app.table_windows.empty()) {
ImGui::TextDisabled("(no expanded tables)");
if (app.node_groups_windows.empty()) {
ImGui::TextDisabled("(no open NodeGroups)");
} else {
for (auto& kv : app.table_windows) {
for (auto& kv : app.node_groups_windows) {
bool checked = kv.second.open;
char lbl[160];
std::snprintf(lbl, sizeof(lbl), "%s (%lld rows)",
@@ -326,7 +326,7 @@ void views_toolbar(AppState& app) {
}
ImGui::Separator();
if (ImGui::MenuItem(TI_X " Collapse all")) {
for (auto& kv : app.table_windows) kv.second.open = false;
for (auto& kv : app.node_groups_windows) kv.second.open = false;
}
}
ImGui::EndPopup();
@@ -1873,12 +1873,12 @@ void views_table(AppState& app) {
// Table node UI fase 2 (issue 0011) — ventana expandida + import
// ----------------------------------------------------------------------------
void views_table_windows_sync(AppState& app, const char* ops_db) {
void views_node_groups_windows_sync(AppState& app, const char* ops_db) {
if (!app.graph || !ops_db) return;
GraphData& g = *app.graph;
// Construir set de Tables expandidas con su metadata fresca.
std::unordered_map<std::string, TableMetadata> live;
std::unordered_map<std::string, NodeGroupsMeta> live;
for (int i = 0; i < g.node_count; ++i) {
const GraphNode& n = g.nodes[i];
if (n.type_id >= (uint16_t)g.type_count) continue;
@@ -1906,8 +1906,8 @@ void views_table_windows_sync(AppState& app, const char* ops_db) {
const unsigned char* p = sqlite3_column_text(st, 0);
if (!p) continue;
std::string id = (const char*)p;
TableMetadata meta;
if (tableview_get_metadata(ops_db, id.c_str(), &meta)) {
NodeGroupsMeta meta;
if (node_groups_get_metadata(ops_db, id.c_str(), &meta)) {
live.emplace(id, std::move(meta));
}
}
@@ -1915,8 +1915,8 @@ void views_table_windows_sync(AppState& app, const char* ops_db) {
sqlite3_close(db);
// Quitar las que ya no estan expanded.
for (auto it = app.table_windows.begin(); it != app.table_windows.end(); ) {
if (live.find(it->first) == live.end()) it = app.table_windows.erase(it);
for (auto it = app.node_groups_windows.begin(); it != app.node_groups_windows.end(); ) {
if (live.find(it->first) == live.end()) it = app.node_groups_windows.erase(it);
else ++it;
}
// Anadir las nuevas o refrescar metadata. Tras cualquier sync forzamos
@@ -1925,7 +1925,7 @@ void views_table_windows_sync(AppState& app, const char* ops_db) {
// promote/demote/import — donde el flag promoted de cada fila puede
// haber cambiado).
for (auto& kv : live) {
auto& w = app.table_windows[kv.first];
auto& w = app.node_groups_windows[kv.first];
bool was_present = !w.meta.entity_id.empty();
w.meta = std::move(kv.second);
w.open = true;
@@ -1938,18 +1938,18 @@ void views_table_windows_sync(AppState& app, const char* ops_db) {
}
}
void views_table_window(AppState& app) {
if (app.table_windows.empty()) return;
void views_node_groups_window(AppState& app) {
if (app.node_groups_windows.empty()) return;
GraphData* g = app.graph;
GraphViewportState* vp = app.viewport;
for (auto& kv : app.table_windows) {
TableMetadata& m = kv.second.meta;
AppState::TableWindowState& w = kv.second;
for (auto& kv : app.node_groups_windows) {
NodeGroupsMeta& m = kv.second.meta;
AppState::NodeGroupsWindowState& w = kv.second;
char title[160];
std::snprintf(title, sizeof(title), TI_TABLE " %s##te_%s",
m.name.empty() ? "Table" : m.name.c_str(),
std::snprintf(title, sizeof(title), TI_TABLE " NodeGroups: %s##te_%s",
m.name.empty() ? "(unnamed)" : m.name.c_str(),
m.entity_id.c_str());
ImGui::SetNextWindowSize(ImVec2(640, 460), ImGuiCond_FirstUseEver);
if (!ImGui::Begin(title, &w.open)) { ImGui::End(); continue; }
@@ -1987,7 +1987,7 @@ void views_table_window(AppState& app) {
// avanzamos offset.
const int64_t page_size = 200;
for (int64_t i = 0; i < (int64_t)w.page.size(); ++i) {
const TablePageRow& row = w.page[i];
const NodeGroupsRow& row = w.page[i];
ImGui::TableNextRow();
ImGui::PushID((int)(w.offset + i));
@@ -2080,7 +2080,7 @@ void views_table_window(AppState& app) {
}
// Cerrar la ventana = expanded=false. Lo procesa main.cpp leyendo
// table_windows y comparando `open`.
// node_groups_windows y comparando `open`.
}
bool views_import_dataset_modal(AppState& app) {
@@ -2132,7 +2132,7 @@ bool views_import_dataset_modal(AppState& app) {
// Table node overlay (issue 0010)
// ----------------------------------------------------------------------------
void views_table_overlay(AppState& app) {
void views_node_groups_overlay(AppState& app) {
if (!app.graph || !app.viewport) return;
GraphData& g = *app.graph;
if (g.type_count == 0) return;
@@ -2162,8 +2162,8 @@ void views_table_overlay(AppState& app) {
if (vy < wmin.y - 100 || vy > wmax.y + 100) continue;
int64_t count = -1;
auto it = app.table_node_counts.find(n.user_data);
if (it != app.table_node_counts.end()) count = it->second;
auto it = app.node_groups_counts.find(n.user_data);
if (it != app.node_groups_counts.end()) count = it->second;
if (count < 0) continue;
char buf[64];
+35 -33
View File
@@ -5,7 +5,7 @@
#include "types_registry.h"
#include "entity_ops.h"
#include "tableview.h"
#include "node_groups.h"
#include <cstdint>
#include <unordered_map>
@@ -158,23 +158,24 @@ struct AppState {
// ---- Table node (issue 0010) ------------------------------------------
// Cache de conteo de filas por nodo Table indexado por user_data hash.
// Refrescado tras load_input y tras mutaciones que afecten a Tables.
std::unordered_map<uint64_t, int64_t> table_node_counts;
std::unordered_map<uint64_t, int64_t> node_groups_counts;
// ---- Table node UI fase 2 (issue 0011) --------------------------------
// Estado runtime por ventana de Table expandida. Una entrada por
// entity_id de Table que el usuario haya expandido. La ventana se cierra
// cuando set_expanded(false) — ya sea desde context menu o cerrando la
// ImGui window (que pone el flag a false automaticamente).
struct TableWindowState {
TableMetadata meta; // refrescada cada vez que entity cambia
int64_t total_rows = 0;
int64_t offset = 0;
std::vector<TablePageRow> page;
bool page_dirty = true;
bool open = true; // bound a ImGui::Begin
std::string last_error; // ultimo error de query (vacio = OK)
// ---- NodeGroups window (issue 0011, renombrado en 0036a) --------------
// Estado runtime por ventana de NodeGroups (un Table-typed expandido).
// Una entrada por entity_id de Table que el usuario haya expandido. La
// ventana se cierra cuando set_expanded(false) — ya sea desde context
// menu o cerrando la ImGui window (que pone el flag a false
// automaticamente).
struct NodeGroupsWindowState {
NodeGroupsMeta meta; // refrescada cada vez que entity cambia
int64_t total_rows = 0;
int64_t offset = 0;
std::vector<NodeGroupsRow> page;
bool page_dirty = true;
bool open = true; // bound a ImGui::Begin
std::string last_error; // ultimo error de query (vacio = OK)
};
std::unordered_map<std::string, TableWindowState> table_windows;
std::unordered_map<std::string, NodeGroupsWindowState> node_groups_windows;
// Triggers consumidos por main.cpp tras click en filas.
bool want_promote_row = false;
@@ -196,9 +197,9 @@ struct AppState {
bool want_import = false;
std::string import_error;
// Toggle expanded desde context menu del viewport.
bool want_toggle_expanded = false;
std::string toggle_expanded_id;
// Toggle NodeGroups window desde context menu del viewport.
bool want_toggle_nodegroups = false;
std::string toggle_nodegroups_id;
// ---- Table view (issue 0004) -------------------------------------------
// Vista tabular dockeable. Tabs por type_ref del grafo activo + opcional
@@ -335,33 +336,34 @@ EntityRecord views_inspector_build_record(const AppState& app);
// al cambiar de proyecto.
void views_inspector_clear_draft(AppState& app);
// ---- Table node UI fase 2 (issue 0011) ----------------------------------
// ---- NodeGroups window (issue 0011, renombrado en 0036a) ----------------
// Renderiza una ventana ImGui dockeable por cada Table en table_windows
// con `open=true`. Cabecera con nombres de columnas. Filas paginadas con
// ImGuiListClipper consumiendo el page cache; al cambiar el offset, marca
// dirty para que main.cpp refresque via tableview_page. Doble click en
// fila no promovida -> setea promote_table_id/promote_row_id; promovida
// -> focus_entity_id. Cerrar la ventana setea expanded=false en BD.
void views_table_window(AppState& app);
// Renderiza una ventana ImGui dockeable por cada NodeGroups en
// node_groups_windows con `open=true`. Cabecera con nombres de columnas.
// Filas paginadas con ImGuiListClipper consumiendo el page cache; al
// cambiar el offset, marca dirty para que main.cpp refresque via
// node_groups_page. Doble click en fila no promovida -> setea
// promote_table_id/promote_row_id; promovida -> focus_entity_id. Cerrar la
// ventana setea expanded=false en BD.
void views_node_groups_window(AppState& app);
// Modal "Import dataset..." — formulario para crear una tabla DuckDB
// desde CSV/Parquet/JSON y registrar el nodo Table correspondiente.
bool views_import_dataset_modal(AppState& app);
// Sincroniza table_windows con la metadata.expanded de cada nodo Table.
// Llamar tras load + tras mutaciones que cambien expanded. Crea entradas
// para nuevos expanded y borra las que ya no aplican.
void views_table_windows_sync(AppState& app, const char* ops_db);
// Sincroniza node_groups_windows con la metadata.expanded de cada nodo
// Table. Llamar tras load + tras mutaciones que cambien expanded. Crea
// entradas para nuevos expanded y borra las que ya no aplican.
void views_node_groups_windows_sync(AppState& app, const char* ops_db);
// ---- Table node overlay (issue 0010) ------------------------------------
// Dibuja un overlay rectangulo redondeado sobre cada nodo `Table` del grafo
// con etiqueta "Table · N rows" leyendo de app.table_node_counts. Llamar
// con etiqueta "Table · N rows" leyendo de app.node_groups_counts. Llamar
// despues de graph_viewport(...) — usa GetItemRectMin/Max + GetWindowDrawList
// del item viewport. No interactua con eventos; el hit-testing del nodo
// sigue usandolo el viewport circular de fondo.
void views_table_overlay(AppState& app);
void views_node_groups_overlay(AppState& app);
// ---- Table view (issue 0004) --------------------------------------------