---
id: 0010
title: Nodo tabla — DuckDB foundation + render colapsado
status: completed
priority: high
created: 2026-04-30
revised: 2026-05-01
completed: 2026-05-01
depends_on: [0004, 0005, 0008]
---

## Objetivo

Tier de almacenamiento tabular para nodos `Table` que pueden contener millones
de filas sin saturar `operations.db` ni el grafo. Las "filas" viven en un
`.duckdb` por proyecto (o por tabla) y se promueven a entidades reales del
grafo solo cuando se necesita interactuar con ellas individualmente
(relaciones, edicion, etc.).

Esta issue cubre **fase 1** — vendoring de DuckDB, funciones `tableview_*`
core, y render colapsado del nodo Table en el viewport. La fase 2 (UI
expandida, paginacion, promote/demote, ingesta CSV/Parquet) va en issue
0011.

## Modelo de datos

**Dos tiers** por proyecto:

```
projects/<proj>/apps/graph_explorer/
  operations.db                # SQLite — grafo (entities + relations + filas promovidas)
  tables/
    <slug>.duckdb              # DuckDB — bulk tabular (millones de filas)
```

El nodo Table es una entidad normal con `type_ref = 'Table'` y metadata que
apunta a su dataset DuckDB. **No contiene filas internamente** — es una vista.

```json
{
  "duckdb_path":  "tables/sospechosos.duckdb",
  "table_name":   "people",
  "row_type":     "Person",
  "id_column":    "id",
  "label_column": "name",
  "columns":      ["name","age","email"],
  "filter_sql":   "",
  "expanded":     false
}
```

Una fila promovida es una entidad en `operations.db` con metadata de origen:

```json
{
  "source": {
    "duckdb": "tables/sospechosos.duckdb",
    "table":  "people",
    "row_id": "p_42"
  },
  "name": "Ana Lopez",
  "age":  33
}
```

## Cambios en codigo

- **Vendor DuckDB** en `cpp/vendor/duckdb/` (amalgamation o precompiled). Add
  library en `cpp/CMakeLists.txt`.
- Nuevo paquete `cpp/functions/duck/`:
  - `duck_open(path) -> duckdb_database` (con `duckdb_open` + `duckdb_connect`).
  - `duck_query(conn, sql, params) -> result` wrapper.
- `entity_ops` (o `tableview.{h,cpp}` en la app) — funciones a nivel de app:
  - `tableview_create(ops_db, duckdb_path, duck_table, row_type, char* out_id)`
    crea entidad `Table` con metadata + commit en `operations.db`.
  - `tableview_count(duckdb_path, sql_filter, int64_t* out)`.
  - `tableview_page(duckdb_path, sql_filter, offset, limit, vector<TablePageRow>* out)`.
  - `TablePageRow` lleva los campos del `columns[]` resueltos a string +
    `promoted` (LEFT JOIN contra `ops.entities`).
- `graph_load_from_operations`: filtrar relaciones `CONTAINS_ROW` (heredado
  del modelo viejo, ya no se emiten pero por si se topa con dbs antiguas).
- `views.cpp`:
  - Detectar nodos `type_ref == "Table"` al renderizar etiquetas/contadores.
  - Overlay con `ImGui::GetForegroundDrawList()` por cada nodo Table:
    rectangulo redondeado + label "Table · N filas".

## Definicion de hecho

- DuckDB compila y linka en linux + windows (cmake target).
- Smoke test: abrir un `.duckdb` vacio, crear tabla con 1M filas (CTAS desde
  `range`), correr `SELECT COUNT(*)` < 100 ms.
- `tableview_create` + `tableview_count` + `tableview_page` con tests.
- Un nodo `type_ref='Table'` en el grafo se renderiza con un cuadrado overlay
  encima del circulo GPU, con contador de filas obtenido por `tableview_count`.
- El contador refresca al recargar el grafo o tras un INSERT en su DuckDB.
